basetextdocument.cpp 15.7 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8
9
10
11
12
13
14
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
18
19
20
21
22
23
24
25
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
26
27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
#include "basetextdocument.h"
31
32

#include "basetextdocumentlayout.h"
con's avatar
con committed
33
#include "basetexteditor.h"
Jarek Kobus's avatar
Jarek Kobus committed
34
#include "typingsettings.h"
con's avatar
con committed
35
#include "storagesettings.h"
36
#include "tabsettings.h"
37
#include "extraencodingsettings.h"
38
#include "syntaxhighlighter.h"
39
#include "texteditorconstants.h"
con's avatar
con committed
40

41
#include <QApplication>
42
#include <QDir>
43
#include <QFile>
44
45
#include <QFileInfo>
#include <QFutureInterface>
46
47
#include <QScrollBar>
#include <QStringList>
48
#include <QSyntaxHighlighter>
49
50
#include <QTextCodec>
#include <QTextStream>
con's avatar
con committed
51

52
#include <coreplugin/editormanager/editormanager.h>
53
#include <coreplugin/icore.h>
54
#include <coreplugin/progressmanager/progressmanager.h>
hjk's avatar
hjk committed
55
#include <utils/qtcassert.h>
56
#include <utils/fileutils.h>
57
#include <utils/reloadpromptutils.h>
con's avatar
con committed
58

59
namespace TextEditor {
60
61
class BaseTextDocumentPrivate
{
62
63
64
65
66
67
68
public:
    explicit BaseTextDocumentPrivate(BaseTextDocument *q);

    QString m_fileName;
    QString m_defaultPath;
    QString m_suggestedFileName;
    QString m_mimeType;
Jarek Kobus's avatar
Jarek Kobus committed
69
    TypingSettings m_typingSettings;
70
71
    StorageSettings m_storageSettings;
    TabSettings m_tabSettings;
72
    ExtraEncodingSettings m_extraEncodingSettings;
73
74
75
76
    QTextDocument *m_document;
    SyntaxHighlighter *m_highlighter;

    bool m_fileIsReadOnly;
77
    int m_autoSaveRevision;
78
79
80
81
82
83
};

BaseTextDocumentPrivate::BaseTextDocumentPrivate(BaseTextDocument *q) :
    m_document(new QTextDocument(q)),
    m_highlighter(0),
    m_fileIsReadOnly(false),
84
    m_autoSaveRevision(-1)
85
86
{
}
87

88
BaseTextDocument::BaseTextDocument() : d(new BaseTextDocumentPrivate(this))
con's avatar
con committed
89
90
91
92
93
{
}

BaseTextDocument::~BaseTextDocument()
{
94
95
96
    delete d->m_document;
    d->m_document = 0;
    delete d;
con's avatar
con committed
97
98
99
100
}

QString BaseTextDocument::mimeType() const
{
101
    return d->m_mimeType;
con's avatar
con committed
102
103
104
105
}

void BaseTextDocument::setMimeType(const QString &mt)
{
106
107
108
    d->m_mimeType = mt;
}

Jarek Kobus's avatar
Jarek Kobus committed
109
110
111
112
113
void BaseTextDocument::setTypingSettings(const TypingSettings &typingSettings)
{
    d->m_typingSettings = typingSettings;
}

114
115
116
117
118
void BaseTextDocument::setStorageSettings(const StorageSettings &storageSettings)
{
    d->m_storageSettings = storageSettings;
}

Jarek Kobus's avatar
Jarek Kobus committed
119
120
121
122
123
const TypingSettings &BaseTextDocument::typingSettings() const
{
    return d->m_typingSettings;
}

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
const StorageSettings &BaseTextDocument::storageSettings() const
{
    return d->m_storageSettings;
}

void BaseTextDocument::setTabSettings(const TabSettings &tabSettings)
{
    d->m_tabSettings = tabSettings;
}

const TabSettings &BaseTextDocument::tabSettings() const
{
    return d->m_tabSettings;
}

139
140
141
142
143
144
145
146
147
148
void BaseTextDocument::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings)
{
    d->m_extraEncodingSettings = extraEncodingSettings;
}

const ExtraEncodingSettings &BaseTextDocument::extraEncodingSettings() const
{
    return d->m_extraEncodingSettings;
}

149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
QString BaseTextDocument::fileName() const
{
    return d->m_fileName;
}

bool BaseTextDocument::isSaveAsAllowed() const
{
    return true;
}

QString BaseTextDocument::defaultPath() const
{
    return d->m_defaultPath;
}

QString BaseTextDocument::suggestedFileName() const
{
    return d->m_suggestedFileName;
}

void BaseTextDocument::setDefaultPath(const QString &defaultPath)
{
    d->m_defaultPath = defaultPath;
}

void BaseTextDocument::setSuggestedFileName(const QString &suggestedFileName)
{
    d->m_suggestedFileName = suggestedFileName;
}

QTextDocument *BaseTextDocument::document() const
{
    return d->m_document;
}

SyntaxHighlighter *BaseTextDocument::syntaxHighlighter() const
{
    return d->m_highlighter;
}

ITextMarkable *BaseTextDocument::documentMarker() const
{
191
192
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout *>(d->m_document->documentLayout());
193
    QTC_ASSERT(documentLayout, return 0);
194
    return documentLayout->markableInterface();
con's avatar
con committed
195
196
}

197
198
199
200
/*!
 * \brief Saves the document to the specified file.
 * \param errorString output parameter, contains error reason.
 * \param autoSave signalise that this function was called by the automatic save routine.
201
202
 * If autosave is true, the cursor will be restored and some signals suppressed
 * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()).
203
 */
204
bool BaseTextDocument::save(QString *errorString, const QString &fileName, bool autoSave)
con's avatar
con committed
205
{
206
    QTextCursor cursor(d->m_document);
con's avatar
con committed
207

208
209
    // When autosaving, we don't want to modify the document/location under the user's fingers.
    BaseTextEditorWidget *editorWidget = 0;
210
211
    int savedPosition = 0;
    int savedAnchor = 0;
212
213
    int savedVScrollBarValue = 0;
    int savedHScrollBarValue = 0;
214
215
    int undos = d->m_document->availableUndoSteps();

216
217
    // When saving the current editor, make sure to maintain the cursor and scroll bar
    // positions for undo
hjk's avatar
hjk committed
218
    Core::IEditor *currentEditor = Core::EditorManager::currentEditor();
219
    if (BaseTextEditor *editable = qobject_cast<BaseTextEditor*>(currentEditor)) {
220
        if (editable->document() == this) {
221
222
223
224
            editorWidget = editable->editorWidget();
            QTextCursor cur = editorWidget->textCursor();
            savedPosition = cur.position();
            savedAnchor = cur.anchor();
225
226
            savedVScrollBarValue = editorWidget->verticalScrollBar()->value();
            savedHScrollBarValue = editorWidget->horizontalScrollBar()->value();
227
228
            cursor.setPosition(cur.position());
        }
229
230
    }

231
232
233
    if (!autoSave) {
        cursor.beginEditBlock();
        cursor.movePosition(QTextCursor::Start);
234

235
236
237
238
239
240
        if (d->m_storageSettings.m_cleanWhitespace)
          cleanWhitespace(cursor, d->m_storageSettings.m_cleanIndentation, d->m_storageSettings.m_inEntireDocument);
        if (d->m_storageSettings.m_addFinalNewLine)
          ensureFinalNewLine(cursor);
        cursor.endEditBlock();
      }
con's avatar
con committed
241

242
    QString fName = d->m_fileName;
con's avatar
con committed
243
244
245
    if (!fileName.isEmpty())
        fName = fileName;

246
    // check if UTF8-BOM has to be added or removed
247
    Utils::TextFileFormat saveFormat = format();
248
    if (saveFormat.codec->name() == "UTF-8" && supportsUtf8Bom()) {
249
250
251
252
253
254
255
256
257
        switch (d->m_extraEncodingSettings.m_utf8BomSetting) {
        case TextEditor::ExtraEncodingSettings::AlwaysAdd:
            saveFormat.hasUtf8Bom = true;
            break;
        case TextEditor::ExtraEncodingSettings::OnlyKeep:
            break;
        case TextEditor::ExtraEncodingSettings::AlwaysDelete:
            saveFormat.hasUtf8Bom = false;
            break;
258
        }
259
    }
con's avatar
con committed
260

261
    const bool ok = write(fName, saveFormat, d->m_document->toPlainText(), errorString);
262

263
    // restore text cursor and scroll bar positions
264
265
266
267
268
269
    if (autoSave && undos < d->m_document->availableUndoSteps()) {
        d->m_document->undo();
        if (editorWidget) {
            QTextCursor cur = editorWidget->textCursor();
            cur.setPosition(savedAnchor);
            cur.setPosition(savedPosition, QTextCursor::KeepAnchor);
270
271
            editorWidget->verticalScrollBar()->setValue(savedVScrollBarValue);
            editorWidget->horizontalScrollBar()->setValue(savedHScrollBarValue);
272
273
274
275
            editorWidget->setTextCursor(cur);
        }
    }

276
    if (!ok)
con's avatar
con committed
277
        return false;
278
279
280
    d->m_autoSaveRevision = d->m_document->revision();
    if (autoSave)
        return true;
con's avatar
con committed
281

282
    // inform about the new filename
con's avatar
con committed
283
    const QFileInfo fi(fName);
284
    const QString oldFileName = d->m_fileName;
285
286
    d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
    d->m_document->setModified(false);
287
    emit fileNameChanged(oldFileName, d->m_fileName);
con's avatar
con committed
288
289
290
291
292
    emit titleChanged(fi.fileName());
    emit changed();
    return true;
}

293
294
295
296
297
bool BaseTextDocument::shouldAutoSave() const
{
    return d->m_autoSaveRevision != d->m_document->revision();
}

dt's avatar
dt committed
298
299
300
void BaseTextDocument::rename(const QString &newName)
{
    const QFileInfo fi(newName);
301
    const QString oldFileName = d->m_fileName;
302
    d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
303
    emit fileNameChanged(oldFileName, d->m_fileName);
dt's avatar
dt committed
304
305
306
307
    emit titleChanged(fi.fileName());
    emit changed();
}

308
bool BaseTextDocument::isFileReadOnly() const
con's avatar
con committed
309
{
310
    if (d->m_fileName.isEmpty()) //have no corresponding file, so editing is ok
con's avatar
con committed
311
        return false;
312
    return d->m_fileIsReadOnly;
313
314
315
316
}

bool BaseTextDocument::isModified() const
{
317
    return d->m_document->isModified();
318
319
320
321
}

void BaseTextDocument::checkPermissions()
{
322
323
324
325
    bool previousReadOnly = d->m_fileIsReadOnly;
    if (!d->m_fileName.isEmpty()) {
        const QFileInfo fi(d->m_fileName);
        d->m_fileIsReadOnly = !fi.isWritable();
326
    } else {
327
        d->m_fileIsReadOnly = false;
328
    }
329
    if (previousReadOnly != d->m_fileIsReadOnly)
330
        emit changed();
con's avatar
con committed
331
332
}

333
bool BaseTextDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
con's avatar
con committed
334
335
{
    QString title = tr("untitled");
336
337
338
339
    QStringList content;

    ReadResult readResult = Utils::TextFileFormat::ReadIOError;

con's avatar
con committed
340
341
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
342
343
        d->m_fileIsReadOnly = !fi.isWritable();
        d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
con's avatar
con committed
344
345

        title = fi.fileName();
346
        readResult = read(realFileName, &content, errorString);
con's avatar
con committed
347

348
        d->m_document->setModified(false);
349
        const int chunks = content.size();
350
351
352
        if (chunks == 0) {
            d->m_document->setPlainText(QString());
        } else if (chunks == 1) {
353
            d->m_document->setPlainText(content.at(0));
354
        } else {
355
356
            QFutureInterface<void> interface;
            interface.setProgressRange(0, chunks);
hjk's avatar
hjk committed
357
            Core::ICore::progressManager()->addTask(
358
                interface.future(), tr("Opening file"), QLatin1String(Constants::TASK_OPEN_FILE));
359
            interface.reportStarted();
360
            d->m_document->setUndoRedoEnabled(false);
361
            QTextCursor c(d->m_document);
362
363
            c.beginEditBlock();
            d->m_document->clear();
364
365
366
367
            for (int i = 0; i < chunks; ++i) {
                c.insertText(content.at(i));
                interface.setProgressValue(i + 1);
                QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
368
            }
369
            c.endEditBlock();
370
            d->m_document->setUndoRedoEnabled(true);
371
            interface.reportFinished();
372
373
374
        }
        BaseTextDocumentLayout *documentLayout =
            qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
hjk's avatar
hjk committed
375
        QTC_ASSERT(documentLayout, return true);
376
377
        documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document->revision();
        d->m_document->setModified(fileName != realFileName);
con's avatar
con committed
378
379
380
        emit titleChanged(title);
        emit changed();
    }
Robert Loehning's avatar
Robert Loehning committed
381
382
    return readResult == Utils::TextFileFormat::ReadSuccess
           || readResult == Utils::TextFileFormat::ReadEncodingError;
con's avatar
con committed
383
384
}

385
bool BaseTextDocument::reload(QString *errorString, QTextCodec *codec)
con's avatar
con committed
386
{
387
    QTC_ASSERT(codec, return false);
388
    setCodec(codec);
389
    return reload(errorString);
con's avatar
con committed
390
391
}

392
bool BaseTextDocument::reload(QString *errorString)
con's avatar
con committed
393
394
{
    emit aboutToReload();
395
396
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
397
    TextMarks marks;
398
    if (documentLayout)
399
        marks = documentLayout->documentClosing(); // removes text marks non-permanently
400

401
402
403
404
405
406
407
    bool success = open(errorString, d->m_fileName, d->m_fileName);

    if (documentLayout)
        documentLayout->documentReloaded(marks); // readds text marks
    if (success)
        emit reloaded();
    return success;
con's avatar
con committed
408
409
}

410
bool BaseTextDocument::reload(QString *errorString, ReloadFlag flag, ChangeType type)
411
412
{
    if (flag == FlagIgnore)
413
        return true;
414
    if (type == TypePermissions) {
415
        checkPermissions();
416
        return true;
417
    } else {
418
        return reload(errorString);
con's avatar
con committed
419
420
421
    }
}

422
void BaseTextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter)
con's avatar
con committed
423
{
424
425
426
427
428
    if (d->m_highlighter)
        delete d->m_highlighter;
    d->m_highlighter = highlighter;
    d->m_highlighter->setParent(this);
    d->m_highlighter->setDocument(d->m_document);
con's avatar
con committed
429
430
}

431
432


433
void BaseTextDocument::cleanWhitespace(const QTextCursor &cursor)
434
{
435
436
    bool hasSelection = cursor.hasSelection();
    QTextCursor copyCursor = cursor;
mae's avatar
mae committed
437
    copyCursor.setVisualNavigation(false);
438
439
440
441
442
    copyCursor.beginEditBlock();
    cleanWhitespace(copyCursor, true, true);
    if (!hasSelection)
        ensureFinalNewLine(copyCursor);
    copyCursor.endEditBlock();
443
444
}

445
void BaseTextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument)
con's avatar
con committed
446
{
447
    BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
mae's avatar
mae committed
448
    Q_ASSERT(cursor.visualNavigation() == false);
con's avatar
con committed
449

450
    QTextBlock block = d->m_document->findBlock(cursor.selectionStart());
451
452
    QTextBlock end;
    if (cursor.hasSelection())
453
        end = d->m_document->findBlock(cursor.selectionEnd()-1).next();
454
455

    while (block.isValid() && block != end) {
con's avatar
con committed
456

457
        if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) {
con's avatar
con committed
458
459

            QString blockText = block.text();
460
            if (int trailing = d->m_tabSettings.trailingWhitespaces(blockText)) {
con's avatar
con committed
461
462
463
464
                cursor.setPosition(block.position() + block.length() - 1);
                cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, trailing);
                cursor.removeSelectedText();
            }
465
            if (cleanIndentation && !d->m_tabSettings.isIndentationClean(block)) {
con's avatar
con committed
466
                cursor.setPosition(block.position());
467
                int firstNonSpace = d->m_tabSettings.firstNonSpace(blockText);
con's avatar
con committed
468
469
470
471
                if (firstNonSpace == blockText.length()) {
                    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                    cursor.removeSelectedText();
                } else {
472
                    int column = d->m_tabSettings.columnAt(blockText, firstNonSpace);
con's avatar
con committed
473
                    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
474
                    QString indentationString = d->m_tabSettings.indentationString(0, column, block);
con's avatar
con committed
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
                    cursor.insertText(indentationString);
                }
            }
        }

        block = block.next();
    }
}

void BaseTextDocument::ensureFinalNewLine(QTextCursor& cursor)
{
    cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
    bool emptyFile = !cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);

    if (!emptyFile && cursor.selectedText().at(0) != QChar::ParagraphSeparator)
    {
        cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
        cursor.insertText(QLatin1String("\n"));
    }
}
495

496
} // namespace TextEditor