basetextdocument.cpp 15.6 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"
34
#include "convenience.h"
Jarek Kobus's avatar
Jarek Kobus committed
35
#include "typingsettings.h"
con's avatar
con committed
36
#include "storagesettings.h"
37
#include "tabsettings.h"
38
#include "extraencodingsettings.h"
39
#include "syntaxhighlighter.h"
40
#include "texteditorconstants.h"
con's avatar
con committed
41

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

50
#include <coreplugin/icore.h>
51
#include <coreplugin/progressmanager/progressmanager.h>
hjk's avatar
hjk committed
52
#include <utils/qtcassert.h>
con's avatar
con committed
53

54 55
using namespace Core;

56
namespace TextEditor {
57 58
class BaseTextDocumentPrivate
{
59 60 61 62 63 64
public:
    explicit BaseTextDocumentPrivate(BaseTextDocument *q);

    QString m_defaultPath;
    QString m_suggestedFileName;
    QString m_mimeType;
Jarek Kobus's avatar
Jarek Kobus committed
65
    TypingSettings m_typingSettings;
66 67
    StorageSettings m_storageSettings;
    TabSettings m_tabSettings;
68
    ExtraEncodingSettings m_extraEncodingSettings;
69 70 71 72
    QTextDocument *m_document;
    SyntaxHighlighter *m_highlighter;

    bool m_fileIsReadOnly;
73
    int m_autoSaveRevision;
74 75 76 77 78 79
};

BaseTextDocumentPrivate::BaseTextDocumentPrivate(BaseTextDocument *q) :
    m_document(new QTextDocument(q)),
    m_highlighter(0),
    m_fileIsReadOnly(false),
80
    m_autoSaveRevision(-1)
81 82
{
}
83

84
BaseTextDocument::BaseTextDocument() : d(new BaseTextDocumentPrivate(this))
con's avatar
con committed
85 86 87 88 89
{
}

BaseTextDocument::~BaseTextDocument()
{
90 91 92
    delete d->m_document;
    d->m_document = 0;
    delete d;
con's avatar
con committed
93 94
}

95
QString BaseTextDocument::plainText() const
96 97 98 99 100 101 102 103 104 105 106 107 108 109
{
    return document()->toPlainText();
}

QString BaseTextDocument::textAt(int pos, int length) const
{
    return Convenience::textAt(QTextCursor(document()), pos, length);
}

QChar BaseTextDocument::characterAt(int pos) const
{
    return document()->characterAt(pos);
}

con's avatar
con committed
110 111
QString BaseTextDocument::mimeType() const
{
112
    return d->m_mimeType;
con's avatar
con committed
113 114 115 116
}

void BaseTextDocument::setMimeType(const QString &mt)
{
117 118 119 120
    if (d->m_mimeType != mt) {
        d->m_mimeType = mt;
        emit mimeTypeChanged();
    }
121 122
}

Jarek Kobus's avatar
Jarek Kobus committed
123 124 125 126 127
void BaseTextDocument::setTypingSettings(const TypingSettings &typingSettings)
{
    d->m_typingSettings = typingSettings;
}

128 129 130 131 132
void BaseTextDocument::setStorageSettings(const StorageSettings &storageSettings)
{
    d->m_storageSettings = storageSettings;
}

Jarek Kobus's avatar
Jarek Kobus committed
133 134 135 136 137
const TypingSettings &BaseTextDocument::typingSettings() const
{
    return d->m_typingSettings;
}

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
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;
}

153 154 155 156 157 158 159 160 161 162
void BaseTextDocument::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings)
{
    d->m_extraEncodingSettings = extraEncodingSettings;
}

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

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 191 192 193 194 195 196 197
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;
}

198
ITextMarkable *BaseTextDocument::markableInterface() const
199
{
200 201
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout *>(d->m_document->documentLayout());
202
    QTC_ASSERT(documentLayout, return 0);
203
    return documentLayout->markableInterface();
con's avatar
con committed
204 205
}

206
/*!
207 208 209 210
 * Saves the document to the file specified by \a fileName. If errors occur,
 * \a errorString contains their cause.
 * \a autoSave returns whether this function was called by the automatic save routine.
 * If \a autoSave is true, the cursor will be restored and some signals suppressed
211
 * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()).
212
 */
213
bool BaseTextDocument::save(QString *errorString, const QString &saveFileName, bool autoSave)
con's avatar
con committed
214
{
215
    QTextCursor cursor(d->m_document);
con's avatar
con committed
216

217 218
    // When autosaving, we don't want to modify the document/location under the user's fingers.
    BaseTextEditorWidget *editorWidget = 0;
219 220
    int savedPosition = 0;
    int savedAnchor = 0;
221 222
    int savedVScrollBarValue = 0;
    int savedHScrollBarValue = 0;
223 224
    int undos = d->m_document->availableUndoSteps();

225 226
    // When saving the current editor, make sure to maintain the cursor and scroll bar
    // positions for undo
227
    IEditor *currentEditor = EditorManager::currentEditor();
228
    if (BaseTextEditor *editable = qobject_cast<BaseTextEditor*>(currentEditor)) {
229
        if (editable->document() == this) {
230 231 232 233
            editorWidget = editable->editorWidget();
            QTextCursor cur = editorWidget->textCursor();
            savedPosition = cur.position();
            savedAnchor = cur.anchor();
234 235
            savedVScrollBarValue = editorWidget->verticalScrollBar()->value();
            savedHScrollBarValue = editorWidget->horizontalScrollBar()->value();
236 237
            cursor.setPosition(cur.position());
        }
238 239
    }

240 241 242
    if (!autoSave) {
        cursor.beginEditBlock();
        cursor.movePosition(QTextCursor::Start);
243

244 245 246 247 248 249
        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
250

251
    QString fName = filePath();
252 253
    if (!saveFileName.isEmpty())
        fName = saveFileName;
con's avatar
con committed
254

255
    // check if UTF8-BOM has to be added or removed
256
    Utils::TextFileFormat saveFormat = format();
257
    if (saveFormat.codec->name() == "UTF-8" && supportsUtf8Bom()) {
258 259 260 261 262 263 264 265 266
        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;
267
        }
268
    }
con's avatar
con committed
269

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

272
    // restore text cursor and scroll bar positions
273 274 275 276 277 278
    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);
279 280
            editorWidget->verticalScrollBar()->setValue(savedVScrollBarValue);
            editorWidget->horizontalScrollBar()->setValue(savedHScrollBarValue);
281 282 283 284
            editorWidget->setTextCursor(cur);
        }
    }

285
    if (!ok)
con's avatar
con committed
286
        return false;
287 288 289
    d->m_autoSaveRevision = d->m_document->revision();
    if (autoSave)
        return true;
con's avatar
con committed
290

291
    // inform about the new filename
con's avatar
con committed
292
    const QFileInfo fi(fName);
293
    d->m_document->setModified(false);
294
    setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
con's avatar
con committed
295 296 297 298
    emit changed();
    return true;
}

299 300
bool BaseTextDocument::setContents(const QByteArray &contents)
{
301
    if (contents.size() > EditorManager::maxTextFileSize()) {
302 303 304 305 306 307 308 309 310
        document()->setPlainText(BaseTextEditorWidget::msgTextTooLarge(contents.size()));
        document()->setModified(false);
        return false;
    }
    document()->setPlainText(QString::fromUtf8(contents));
    document()->setModified(false);
    return true;
}

311 312 313 314 315
bool BaseTextDocument::shouldAutoSave() const
{
    return d->m_autoSaveRevision != d->m_document->revision();
}

316
void BaseTextDocument::setFilePath(const QString &newName)
dt's avatar
dt committed
317
{
318
    if (newName == filePath())
319
        return;
dt's avatar
dt committed
320
    const QFileInfo fi(newName);
321
    IDocument::setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
dt's avatar
dt committed
322 323
}

324
bool BaseTextDocument::isFileReadOnly() const
con's avatar
con committed
325
{
326
    if (filePath().isEmpty()) //have no corresponding file, so editing is ok
con's avatar
con committed
327
        return false;
328
    return d->m_fileIsReadOnly;
329 330 331 332
}

bool BaseTextDocument::isModified() const
{
333
    return d->m_document->isModified();
334 335 336 337
}

void BaseTextDocument::checkPermissions()
{
338
    bool previousReadOnly = d->m_fileIsReadOnly;
339 340
    if (!filePath().isEmpty()) {
        const QFileInfo fi(filePath());
341
        d->m_fileIsReadOnly = !fi.isWritable();
342
    } else {
343
        d->m_fileIsReadOnly = false;
344
    }
345
    if (previousReadOnly != d->m_fileIsReadOnly)
346
        emit changed();
con's avatar
con committed
347 348
}

349
bool BaseTextDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
con's avatar
con committed
350
{
351 352 353 354
    QStringList content;

    ReadResult readResult = Utils::TextFileFormat::ReadIOError;

con's avatar
con committed
355 356
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
357
        d->m_fileIsReadOnly = !fi.isWritable();
358
        readResult = read(realFileName, &content, errorString);
con's avatar
con committed
359

360
        d->m_document->setModified(false);
361
        const int chunks = content.size();
362 363 364
        if (chunks == 0) {
            d->m_document->setPlainText(QString());
        } else if (chunks == 1) {
365
            d->m_document->setPlainText(content.at(0));
366
        } else {
367 368
            QFutureInterface<void> interface;
            interface.setProgressRange(0, chunks);
369
            ProgressManager::addTask(interface.future(), tr("Opening file"), Constants::TASK_OPEN_FILE);
370
            interface.reportStarted();
371
            d->m_document->setUndoRedoEnabled(false);
372
            QTextCursor c(d->m_document);
373 374
            c.beginEditBlock();
            d->m_document->clear();
375 376 377 378
            for (int i = 0; i < chunks; ++i) {
                c.insertText(content.at(i));
                interface.setProgressValue(i + 1);
                QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
379
            }
380
            c.endEditBlock();
381
            d->m_document->setUndoRedoEnabled(true);
382
            interface.reportFinished();
383 384 385
        }
        BaseTextDocumentLayout *documentLayout =
            qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
hjk's avatar
hjk committed
386
        QTC_ASSERT(documentLayout, return true);
387 388
        documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document->revision();
        d->m_document->setModified(fileName != realFileName);
389
        setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
con's avatar
con committed
390
    }
Robert Loehning's avatar
Robert Loehning committed
391 392
    return readResult == Utils::TextFileFormat::ReadSuccess
           || readResult == Utils::TextFileFormat::ReadEncodingError;
con's avatar
con committed
393 394
}

395
bool BaseTextDocument::reload(QString *errorString, QTextCodec *codec)
con's avatar
con committed
396
{
397
    QTC_ASSERT(codec, return false);
398
    setCodec(codec);
399
    return reload(errorString);
con's avatar
con committed
400 401
}

402
bool BaseTextDocument::reload(QString *errorString)
con's avatar
con committed
403 404
{
    emit aboutToReload();
405 406
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
407
    TextMarks marks;
408
    if (documentLayout)
409
        marks = documentLayout->documentClosing(); // removes text marks non-permanently
410

411
    bool success = open(errorString, filePath(), filePath());
412 413 414

    if (documentLayout)
        documentLayout->documentReloaded(marks); // readds text marks
415
    emit reloadFinished(success);
416
    return success;
con's avatar
con committed
417 418
}

419
bool BaseTextDocument::reload(QString *errorString, ReloadFlag flag, ChangeType type)
420 421
{
    if (flag == FlagIgnore)
422
        return true;
423
    if (type == TypePermissions) {
424
        checkPermissions();
425
        return true;
426
    } else {
427
        return reload(errorString);
con's avatar
con committed
428 429 430
    }
}

431
void BaseTextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter)
con's avatar
con committed
432
{
433 434 435 436 437
    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
438 439
}

440 441


442
void BaseTextDocument::cleanWhitespace(const QTextCursor &cursor)
443
{
444 445
    bool hasSelection = cursor.hasSelection();
    QTextCursor copyCursor = cursor;
mae's avatar
mae committed
446
    copyCursor.setVisualNavigation(false);
447 448 449 450 451
    copyCursor.beginEditBlock();
    cleanWhitespace(copyCursor, true, true);
    if (!hasSelection)
        ensureFinalNewLine(copyCursor);
    copyCursor.endEditBlock();
452 453
}

454
void BaseTextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument)
con's avatar
con committed
455
{
456
    BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
mae's avatar
mae committed
457
    Q_ASSERT(cursor.visualNavigation() == false);
con's avatar
con committed
458

459
    QTextBlock block = d->m_document->findBlock(cursor.selectionStart());
460 461
    QTextBlock end;
    if (cursor.hasSelection())
462
        end = d->m_document->findBlock(cursor.selectionEnd()-1).next();
463 464

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

466
        if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) {
con's avatar
con committed
467 468

            QString blockText = block.text();
469
            d->m_tabSettings.removeTrailingWhitespace(cursor, block);
470
            if (cleanIndentation && !d->m_tabSettings.isIndentationClean(block)) {
con's avatar
con committed
471
                cursor.setPosition(block.position());
472
                int firstNonSpace = d->m_tabSettings.firstNonSpace(blockText);
con's avatar
con committed
473 474 475 476
                if (firstNonSpace == blockText.length()) {
                    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                    cursor.removeSelectedText();
                } else {
477
                    int column = d->m_tabSettings.columnAt(blockText, firstNonSpace);
con's avatar
con committed
478
                    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
479
                    QString indentationString = d->m_tabSettings.indentationString(0, column, block);
con's avatar
con committed
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
                    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"));
    }
}
500

501
} // namespace TextEditor