Coverage Report

Created: 2025-12-08 09:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/sc/source/ui/docshell/arealink.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 *
9
 * This file incorporates work covered by the following license notice:
10
 *
11
 *   Licensed to the Apache Software Foundation (ASF) under one or more
12
 *   contributor license agreements. See the NOTICE file distributed
13
 *   with this work for additional information regarding copyright
14
 *   ownership. The ASF licenses this file to you under the Apache
15
 *   License, Version 2.0 (the "License"); you may not use this file
16
 *   except in compliance with the License. You may obtain a copy of
17
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18
 */
19
20
#include <sfx2/fcontnr.hxx>
21
#include <sfx2/linkmgr.hxx>
22
#include <utility>
23
#include <vcl/svapp.hxx>
24
#include <vcl/weld.hxx>
25
#include <unotools/charclass.hxx>
26
#include <osl/diagnose.h>
27
28
#include <arealink.hxx>
29
30
#include <tablink.hxx>
31
#include <document.hxx>
32
#include <docsh.hxx>
33
#include <rangenam.hxx>
34
#include <dbdata.hxx>
35
#include <undoblk.hxx>
36
#include <globstr.hrc>
37
#include <scresid.hxx>
38
#include <markdata.hxx>
39
#include <hints.hxx>
40
#include <filter.hxx>
41
42
#include <attrib.hxx>
43
#include <patattr.hxx>
44
#include <docpool.hxx>
45
46
#include <scabstdlg.hxx>
47
#include <clipparam.hxx>
48
49
50
ScAreaLink::ScAreaLink( ScDocShell& rShell, OUString aFile,
51
                        OUString aFilter, OUString aOpt,
52
                        OUString aArea, const ScRange& rDest,
53
                        sal_Int32 nRefreshDelaySeconds ) :
54
0
    ::sfx2::SvBaseLink(SfxLinkUpdateMode::ONCALL,SotClipboardFormatId::SIMPLE_FILE),
55
0
    ScRefreshTimer  ( nRefreshDelaySeconds ),
56
0
    m_rDocSh(rShell),
57
0
    aFileName       (std::move(aFile)),
58
0
    aFilterName     (std::move(aFilter)),
59
0
    aOptions        (std::move(aOpt)),
60
0
    aSourceArea     (std::move(aArea)),
61
0
    aDestArea       (rDest),
62
0
    bAddUndo        (true),
63
0
    bInCreate       (false),
64
0
    bDoInsert       (true)
65
0
{
66
0
    SetRefreshHandler( LINK( this, ScAreaLink, RefreshHdl ) );
67
0
    SetRefreshControl( &m_rDocSh.GetDocument().GetRefreshTimerControlAddress() );
68
0
}
69
70
ScAreaLink::~ScAreaLink()
71
0
{
72
0
    StopRefreshTimer();
73
0
}
74
75
void ScAreaLink::Edit(weld::Window* pParent, const Link<SvBaseLink&,void>& /* rEndEditHdl */ )
76
0
{
77
    //  use own dialog instead of SvBaseLink::Edit...
78
0
    ScAbstractDialogFactory* pFact = ScAbstractDialogFactory::Create();
79
80
0
    ScopedVclPtr<AbstractScLinkedAreaDlg> pDlg(pFact->CreateScLinkedAreaDlg(pParent));
81
0
    pDlg->InitFromOldLink( aFileName, aFilterName, aOptions, aSourceArea, GetRefreshDelaySeconds() );
82
0
    if ( pDlg->Execute() == RET_OK )
83
0
    {
84
0
        aOptions = pDlg->GetOptions();
85
0
        Refresh( pDlg->GetURL(), pDlg->GetFilter(),
86
0
                 pDlg->GetSource(), pDlg->GetRefreshDelaySeconds() );
87
88
        //  copy source data from members (set in Refresh) into link name for dialog
89
0
        OUString aNewLinkName;
90
0
        sfx2::MakeLnkName( aNewLinkName, nullptr, aFileName, aSourceArea, &aFilterName );
91
0
        SetName( aNewLinkName );
92
0
    }
93
0
}
94
95
::sfx2::SvBaseLink::UpdateResult ScAreaLink::DataChanged(
96
    const OUString&, const css::uno::Any& )
97
0
{
98
    //  Do not do anything at bInCreate so that update can be called to set
99
    //  the status in the LinkManager without changing the data in the document
100
101
0
    if (bInCreate)
102
0
        return SUCCESS;
103
104
0
    sfx2::LinkManager* pLinkManager=m_rDocSh.GetDocument().GetLinkManager();
105
0
    if (pLinkManager!=nullptr)
106
0
    {
107
0
        OUString aFile, aArea, aFilter;
108
0
        sfx2::LinkManager::GetDisplayNames(this, nullptr, &aFile, &aArea, &aFilter);
109
110
        //  the file dialog returns the filter name with the application prefix
111
        //  -> remove prefix
112
0
        ScDocumentLoader::RemoveAppPrefix( aFilter );
113
114
        // dialog doesn't set area, so keep old one
115
0
        if (aArea.isEmpty())
116
0
        {
117
0
            aArea = aSourceArea;
118
119
            // adjust in dialog:
120
0
            OUString aNewLinkName;
121
0
            OUString aTmp = aFilter;
122
0
            sfx2::MakeLnkName(aNewLinkName, nullptr, aFile, aArea, &aTmp);
123
0
            aFilter = aTmp;
124
0
            SetName( aNewLinkName );
125
0
        }
126
127
0
        tools::SvRef<sfx2::SvBaseLink> const xThis(this); // keep yourself alive
128
0
        Refresh( aFile, aFilter, aArea, GetRefreshDelaySeconds() );
129
0
    }
130
131
0
    return SUCCESS;
132
0
}
133
134
void ScAreaLink::Closed()
135
0
{
136
    // delete link: Undo
137
138
0
    ScDocument& rDoc = m_rDocSh.GetDocument();
139
0
    bool bUndo (rDoc.IsUndoEnabled());
140
0
    if (bAddUndo && bUndo)
141
0
    {
142
0
        m_rDocSh.GetUndoManager()->AddUndoAction( std::make_unique<ScUndoRemoveAreaLink>( m_rDocSh,
143
0
                                                        aFileName, aFilterName, aOptions,
144
0
                                                        aSourceArea, aDestArea, GetRefreshDelaySeconds() ) );
145
146
0
        bAddUndo = false;   // only once
147
0
    }
148
149
0
    SCTAB nDestTab = aDestArea.aStart.Tab();
150
0
    rDoc.SetStreamValid(nDestTab, false);
151
152
0
    SvBaseLink::Closed();
153
0
}
154
155
void ScAreaLink::SetDestArea(const ScRange& rNew)
156
0
{
157
0
    aDestArea = rNew;           // for Undo
158
0
}
159
160
void ScAreaLink::SetSource(const OUString& rDoc, const OUString& rFlt, const OUString& rOpt,
161
                                const OUString& rArea)
162
0
{
163
0
    aFileName   = rDoc;
164
0
    aFilterName = rFlt;
165
0
    aOptions    = rOpt;
166
0
    aSourceArea = rArea;
167
168
    //  also update link name for dialog
169
0
    OUString aNewLinkName;
170
0
    sfx2::MakeLnkName( aNewLinkName, nullptr, aFileName, aSourceArea, &aFilterName );
171
0
    SetName( aNewLinkName );
172
0
}
173
174
bool ScAreaLink::IsEqual( std::u16string_view rFile, std::u16string_view rFilter, std::u16string_view rOpt,
175
                            std::u16string_view rSource, const ScRange& rDest ) const
176
0
{
177
0
    return aFileName == rFile && aFilterName == rFilter && aOptions == rOpt &&
178
0
            aSourceArea == rSource && aDestArea.aStart == rDest.aStart;
179
0
}
180
181
// find a range with name >rAreaName< in >rSrcDoc<, return it in >rRange<
182
bool ScAreaLink::FindExtRange( ScRange& rRange, const ScDocument& rSrcDoc, const OUString& rAreaName )
183
0
{
184
0
    bool bFound = false;
185
0
    OUString aUpperName = ScGlobal::getCharClass().uppercase(rAreaName);
186
0
    ScRangeName* pNames = rSrcDoc.GetRangeName();
187
0
    if (pNames)         // named ranges
188
0
    {
189
0
        const ScRangeData* p = pNames->findByUpperName(aUpperName);
190
0
        if (p && p->IsValidReference(rRange))
191
0
            bFound = true;
192
0
    }
193
0
    if (!bFound)        // database ranges
194
0
    {
195
0
        ScDBCollection* pDBColl = rSrcDoc.GetDBCollection();
196
0
        if (pDBColl)
197
0
        {
198
0
            const ScDBData* pDB = pDBColl->getNamedDBs().findByUpperName(aUpperName);
199
0
            if (pDB)
200
0
            {
201
0
                SCTAB nTab;
202
0
                SCCOL nCol1, nCol2;
203
0
                SCROW nRow1, nRow2;
204
0
                pDB->GetArea(nTab,nCol1,nRow1,nCol2,nRow2);
205
0
                rRange = ScRange( nCol1,nRow1,nTab, nCol2,nRow2,nTab );
206
0
                bFound = true;
207
0
            }
208
0
        }
209
0
    }
210
0
    if (!bFound)        // direct reference (range or cell)
211
0
    {
212
0
        ScAddress::Details aDetails(rSrcDoc.GetAddressConvention(), 0, 0);
213
0
        if ( rRange.ParseAny( rAreaName, rSrcDoc, aDetails ) & ScRefFlags::VALID )
214
0
            bFound = true;
215
0
    }
216
0
    return bFound;
217
0
}
218
219
//  execute:
220
221
bool ScAreaLink::Refresh( const OUString& rNewFile, const OUString& rNewFilter,
222
                            const OUString& rNewArea, sal_Int32 nNewRefreshDelaySeconds )
223
0
{
224
    //  load document - like TabLink
225
226
0
    if (rNewFile.isEmpty() || rNewFilter.isEmpty())
227
0
        return false;
228
229
0
    if (!m_rDocSh.GetEmbeddedObjectContainer().getUserAllowsLinkUpdate())
230
0
        return false;
231
232
0
    OUString aNewUrl( ScGlobal::GetAbsDocName( rNewFile, &m_rDocSh ) );
233
0
    bool bNewUrlName = (aNewUrl != aFileName);
234
235
0
    std::shared_ptr<const SfxFilter> pFilter = m_rDocSh.GetFactory().GetFilterContainer()->GetFilter4FilterName(rNewFilter);
236
0
    if (!pFilter)
237
0
        return false;
238
239
0
    ScDocument& rDoc = m_rDocSh.GetDocument();
240
241
0
    bool bUndo (rDoc.IsUndoEnabled());
242
0
    rDoc.SetInLinkUpdate( true );
243
244
    //  if new filter was selected, forget options
245
0
    if ( rNewFilter != aFilterName )
246
0
        aOptions.clear();
247
248
0
    SfxMedium* pMed = ScDocumentLoader::CreateMedium( aNewUrl, pFilter, aOptions);
249
250
    // aRef->DoClose() will be closed explicitly, but it is still more safe to use SfxObjectShellLock here
251
0
    rtl::Reference<ScDocShell> pSrcShell = new ScDocShell(SfxModelFlags::EMBEDDED_OBJECT | SfxModelFlags::DISABLE_EMBEDDED_SCRIPTS);
252
0
    pSrcShell->DoLoad(pMed);
253
254
0
    ScDocument& rSrcDoc = pSrcShell->GetDocument();
255
256
    // options could have been set
257
0
    OUString aNewOpt = ScDocumentLoader::GetOptions(*pMed);
258
0
    if (aNewOpt.isEmpty())
259
0
        aNewOpt = aOptions;
260
261
    // correct source range name list for web query import
262
0
    OUString aTempArea;
263
264
0
    if( rNewFilter == ScDocShell::GetWebQueryFilterName() )
265
0
        aTempArea = ScFormatFilter::Get().GetHTMLRangeNameList( rSrcDoc, rNewArea );
266
0
    else
267
0
        aTempArea = rNewArea;
268
269
    // find total size of source area
270
0
    SCCOL nWidth = 0;
271
0
    SCROW nHeight = 0;
272
0
    ScRangeList aSourceRanges;
273
274
0
    if (rNewFilter == SC_TEXT_CSV_FILTER_NAME && aTempArea == "CSV_all")
275
0
    {
276
        // The dummy All range. All data, including top/left empty
277
        // rows/columns.
278
0
        aTempArea.clear();
279
0
        SCCOL nEndCol = 0;
280
0
        SCROW nEndRow = 0;
281
0
        if (rSrcDoc.GetCellArea( 0, nEndCol, nEndRow))
282
0
        {
283
0
            aSourceRanges.push_back( ScRange( 0,0,0, nEndCol, nEndRow, 0));
284
0
            nWidth = nEndCol + 1;
285
0
            nHeight = nEndRow + 2;
286
0
        }
287
0
    }
288
289
0
    if (!aTempArea.isEmpty())
290
0
    {
291
0
        sal_Int32 nIdx {0};
292
0
        do
293
0
        {
294
0
            ScRange aTokenRange;
295
0
            if( FindExtRange( aTokenRange, rSrcDoc, aTempArea.getToken( 0, ';', nIdx ) ) )
296
0
            {
297
0
                aSourceRanges.push_back( aTokenRange);
298
                // columns: find maximum
299
0
                nWidth = std::max( nWidth, static_cast<SCCOL>(aTokenRange.aEnd.Col() - aTokenRange.aStart.Col() + 1) );
300
                // rows: add row range + 1 empty row
301
0
                nHeight += aTokenRange.aEnd.Row() - aTokenRange.aStart.Row() + 2;
302
0
            }
303
0
        }
304
0
        while (nIdx>0);
305
0
    }
306
    // remove the last empty row
307
0
    if( nHeight > 0 )
308
0
        nHeight--;
309
310
    //  delete old data / copy new
311
312
0
    ScAddress aDestPos = aDestArea.aStart;
313
0
    SCTAB nDestTab = aDestPos.Tab();
314
0
    ScRange aOldRange = aDestArea;
315
0
    ScRange aNewRange = aDestArea;          // old range, if file not found or similar
316
0
    if (nWidth > 0 && nHeight > 0)
317
0
    {
318
0
        aNewRange.aEnd.SetCol( aNewRange.aStart.Col() + nWidth - 1 );
319
0
        aNewRange.aEnd.SetRow( aNewRange.aStart.Row() + nHeight - 1 );
320
0
    }
321
322
    //! check CanFitBlock only if bDoInsert is set?
323
0
    bool bCanDo = rDoc.ValidColRow( aNewRange.aEnd.Col(), aNewRange.aEnd.Row() ) &&
324
0
                  rDoc.CanFitBlock( aOldRange, aNewRange );
325
0
    if (bCanDo)
326
0
    {
327
0
        ScDocShellModificator aModificator( m_rDocSh );
328
329
0
        SCCOL nOldEndX = aOldRange.aEnd.Col();
330
0
        SCROW nOldEndY = aOldRange.aEnd.Row();
331
0
        SCCOL nNewEndX = aNewRange.aEnd.Col();
332
0
        SCROW nNewEndY = aNewRange.aEnd.Row();
333
0
        ScRange aMaxRange( aDestPos,
334
0
                    ScAddress(std::max(nOldEndX,nNewEndX), std::max(nOldEndY,nNewEndY), nDestTab) );
335
336
        //  initialise Undo
337
338
0
        ScDocumentUniquePtr pUndoDoc;
339
0
        if ( bAddUndo && bUndo )
340
0
        {
341
0
            pUndoDoc.reset(new ScDocument( SCDOCMODE_UNDO ));
342
0
            if ( bDoInsert )
343
0
            {
344
0
                if ( nNewEndX != nOldEndX || nNewEndY != nOldEndY )             // range changed?
345
0
                {
346
0
                    pUndoDoc->InitUndo( rDoc, 0, rDoc.GetTableCount()-1 );
347
0
                    rDoc.CopyToDocument(0, 0, 0, rDoc.MaxCol(), rDoc.MaxRow(), MAXTAB,
348
0
                                        InsertDeleteFlags::FORMULA, false, *pUndoDoc);     // all formulas
349
0
                }
350
0
                else
351
0
                    pUndoDoc->InitUndo( rDoc, nDestTab, nDestTab );             // only destination table
352
0
                rDoc.CopyToDocument(aOldRange, InsertDeleteFlags::ALL & ~InsertDeleteFlags::NOTE, false, *pUndoDoc);
353
0
            }
354
0
            else        // without insertion
355
0
            {
356
0
                pUndoDoc->InitUndo( rDoc, nDestTab, nDestTab );             // only destination table
357
0
                rDoc.CopyToDocument(aMaxRange, InsertDeleteFlags::ALL & ~InsertDeleteFlags::NOTE, false, *pUndoDoc);
358
0
            }
359
0
        }
360
361
        //  insert / delete cells
362
        //  DeleteAreaTab also deletes MERGE_FLAG attributes
363
364
0
        if (bDoInsert)
365
0
            rDoc.FitBlock( aOldRange, aNewRange );         // incl. deletion
366
0
        else
367
0
            rDoc.DeleteAreaTab( aMaxRange, InsertDeleteFlags::ALL & ~InsertDeleteFlags::NOTE );
368
369
        //  copy data
370
371
0
        if (nWidth > 0 && nHeight > 0)
372
0
        {
373
0
            ScDocument aClipDoc( SCDOCMODE_CLIP );
374
0
            ScRange aNewTokenRange( aNewRange.aStart );
375
0
            for (size_t nRange = 0; nRange < aSourceRanges.size(); ++nRange)
376
0
            {
377
0
                ScRange const & rTokenRange( aSourceRanges[nRange]);
378
0
                SCTAB nSrcTab = rTokenRange.aStart.Tab();
379
0
                ScMarkData aSourceMark(rSrcDoc.GetSheetLimits());
380
0
                aSourceMark.SelectOneTable( nSrcTab );      // selecting for CopyToClip
381
0
                aSourceMark.SetMarkArea( rTokenRange );
382
383
0
                ScClipParam aClipParam(rTokenRange, false);
384
0
                rSrcDoc.CopyToClip(aClipParam, &aClipDoc, &aSourceMark, false, false);
385
386
0
                if ( aClipDoc.HasAttrib( 0,0,nSrcTab, rDoc.MaxCol(),rDoc.MaxRow(),nSrcTab,
387
0
                            HasAttrFlags::Merged | HasAttrFlags::Overlapped ) )
388
0
                {
389
                    //! ResetAttrib at document !!!
390
391
0
                    ScPatternAttr aPattern( rSrcDoc.getCellAttributeHelper() );
392
0
                    aPattern.ItemSetPut(ScMergeAttr());             // Defaults
393
0
                    aPattern.ItemSetPut(ScMergeFlagAttr());
394
0
                    aClipDoc.ApplyPatternAreaTab( 0,0, rDoc.MaxCol(),rDoc.MaxRow(), nSrcTab, aPattern );
395
0
                }
396
397
0
                aNewTokenRange.aEnd.SetCol( aNewTokenRange.aStart.Col() + (rTokenRange.aEnd.Col() - rTokenRange.aStart.Col()) );
398
0
                aNewTokenRange.aEnd.SetRow( aNewTokenRange.aStart.Row() + (rTokenRange.aEnd.Row() - rTokenRange.aStart.Row()) );
399
0
                ScMarkData aDestMark(rDoc.GetSheetLimits());
400
0
                aDestMark.SelectOneTable( nDestTab );
401
0
                aDestMark.SetMarkArea( aNewTokenRange );
402
0
                rDoc.CopyFromClip( aNewTokenRange, aDestMark, InsertDeleteFlags::ALL, nullptr, &aClipDoc, false );
403
0
                aNewTokenRange.aStart.SetRow( aNewTokenRange.aEnd.Row() + 2 );
404
0
            }
405
0
        }
406
0
        else
407
0
        {
408
0
            OUString aErr = ScResId(STR_LINKERROR);
409
0
            rDoc.SetString( aDestPos.Col(), aDestPos.Row(), aDestPos.Tab(), aErr );
410
0
        }
411
412
        //  enter Undo
413
414
0
        if ( bAddUndo && bUndo)
415
0
        {
416
0
            ScDocumentUniquePtr pRedoDoc(new ScDocument( SCDOCMODE_UNDO ));
417
0
            pRedoDoc->InitUndo( rDoc, nDestTab, nDestTab );
418
0
            rDoc.CopyToDocument(aNewRange, InsertDeleteFlags::ALL & ~InsertDeleteFlags::NOTE, false, *pRedoDoc);
419
420
0
            m_rDocSh.GetUndoManager()->AddUndoAction(
421
0
                std::make_unique<ScUndoUpdateAreaLink>( m_rDocSh,
422
0
                                            aFileName, aFilterName, aOptions,
423
0
                                            aSourceArea, aOldRange, GetRefreshDelaySeconds(),
424
0
                                            aNewUrl, rNewFilter, aNewOpt,
425
0
                                            rNewArea, aNewRange, nNewRefreshDelaySeconds,
426
0
                                            std::move(pUndoDoc), std::move(pRedoDoc), bDoInsert ) );
427
0
        }
428
429
        //  remember new settings
430
431
0
        if ( bNewUrlName )
432
0
            aFileName = aNewUrl;
433
0
        if ( rNewFilter != aFilterName )
434
0
            aFilterName = rNewFilter;
435
0
        if ( rNewArea != aSourceArea )
436
0
            aSourceArea = rNewArea;
437
0
        if ( aNewOpt != aOptions )
438
0
            aOptions = aNewOpt;
439
440
0
        if ( aNewRange != aDestArea )
441
0
            aDestArea = aNewRange;
442
443
0
        if ( nNewRefreshDelaySeconds != GetRefreshDelaySeconds() )
444
0
            SetRefreshDelay( nNewRefreshDelaySeconds );
445
446
0
        SCCOL nPaintEndX = std::max( aOldRange.aEnd.Col(), aNewRange.aEnd.Col() );
447
0
        SCROW nPaintEndY = std::max( aOldRange.aEnd.Row(), aNewRange.aEnd.Row() );
448
449
0
        if ( aOldRange.aEnd.Col() != aNewRange.aEnd.Col() )
450
0
            nPaintEndX = rDoc.MaxCol();
451
0
        if ( aOldRange.aEnd.Row() != aNewRange.aEnd.Row() )
452
0
            nPaintEndY = rDoc.MaxRow();
453
454
0
        if ( !m_rDocSh.AdjustRowHeight( aDestPos.Row(), nPaintEndY, nDestTab ) )
455
0
            m_rDocSh.PostPaint(
456
0
                ScRange(aDestPos.Col(), aDestPos.Row(), nDestTab, nPaintEndX, nPaintEndY, nDestTab),
457
0
                PaintPartFlags::Grid);
458
0
        aModificator.SetDocumentModified();
459
0
    }
460
0
    else
461
0
    {
462
        //  CanFitBlock sal_False -> Problems with summarized cells or table boundary reached!
463
        //! cell protection ???
464
465
        //! Link dialog must set default parent
466
        //  "cannot insert rows"
467
0
        weld::Window* pWin = Application::GetFrameWeld(m_rDocSh.GetDialogParent());
468
0
        std::unique_ptr<weld::MessageDialog> xInfoBox(Application::CreateMessageDialog(pWin,
469
0
                                                      VclMessageType::Info, VclButtonsType::Ok,
470
0
                                                      ScResId(STR_MSSG_DOSUBTOTALS_2)));
471
0
        xInfoBox->run();
472
0
    }
473
474
    //  clean up
475
476
0
    pSrcShell->DoClose();
477
478
0
    rDoc.SetInLinkUpdate( false );
479
480
0
    if (bCanDo)
481
0
    {
482
        //  notify Uno objects (for XRefreshListener)
483
        //! also notify Uno objects if file name was changed!
484
0
        ScLinkRefreshedHint aHint;
485
0
        aHint.SetAreaLink( aDestPos );
486
0
        rDoc.BroadcastUno( aHint );
487
0
    }
488
489
0
    return bCanDo;
490
0
}
491
492
IMPL_LINK_NOARG(ScAreaLink, RefreshHdl, Timer *, void)
493
0
{
494
0
    Refresh( aFileName, aFilterName, aSourceArea, GetRefreshDelaySeconds() );
495
0
}
496
497
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */