basetextdocument.cpp 17.1 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2014 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"
35
#include "extraencodingsettings.h"
36 37
#include "indenter.h"
#include "storagesettings.h"
38
#include "syntaxhighlighter.h"
39
#include "tabsettings.h"
40
#include "texteditorconstants.h"
41
#include "typingsettings.h"
con's avatar
con committed
42

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

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

55 56
using namespace Core;

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

    QString m_defaultPath;
    QString m_suggestedFileName;
    QString m_mimeType;
Jarek Kobus's avatar
Jarek Kobus committed
66
    TypingSettings m_typingSettings;
67 68
    StorageSettings m_storageSettings;
    TabSettings m_tabSettings;
69
    ExtraEncodingSettings m_extraEncodingSettings;
70 71
    QTextDocument *m_document;
    SyntaxHighlighter *m_highlighter;
72
    QScopedPointer<Indenter> m_indenter;
73 74

    bool m_fileIsReadOnly;
75
    int m_autoSaveRevision;
76 77 78 79 80
};

BaseTextDocumentPrivate::BaseTextDocumentPrivate(BaseTextDocument *q) :
    m_document(new QTextDocument(q)),
    m_highlighter(0),
81
    m_indenter(new Indenter),
82
    m_fileIsReadOnly(false),
83
    m_autoSaveRevision(-1)
84 85
{
}
86

87
BaseTextDocument::BaseTextDocument() : d(new BaseTextDocumentPrivate(this))
con's avatar
con committed
88
{
89
    connect(d->m_document, SIGNAL(modificationChanged(bool)), this, SIGNAL(changed()));
90
    connect(d->m_document, SIGNAL(contentsChanged()), this, SIGNAL(contentsChanged()));
91 92 93 94 95 96 97 98 99 100

    // set new document layout
    QTextOption opt = d->m_document->defaultTextOption();
    opt.setTextDirection(Qt::LeftToRight);
    opt.setFlags(opt.flags() | QTextOption::IncludeTrailingSpaces
            | QTextOption::AddSpaceForLineAndParagraphSeparators
            );
    d->m_document->setDefaultTextOption(opt);
    BaseTextDocumentLayout *documentLayout = new BaseTextDocumentLayout(d->m_document);
    d->m_document->setDocumentLayout(documentLayout);
con's avatar
con committed
101 102 103 104
}

BaseTextDocument::~BaseTextDocument()
{
105 106 107
    delete d->m_document;
    d->m_document = 0;
    delete d;
con's avatar
con committed
108 109
}

110
QString BaseTextDocument::plainText() const
111 112 113 114 115 116 117 118 119 120 121 122 123 124
{
    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
125 126
QString BaseTextDocument::mimeType() const
{
127
    return d->m_mimeType;
con's avatar
con committed
128 129 130 131
}

void BaseTextDocument::setMimeType(const QString &mt)
{
132 133 134 135
    if (d->m_mimeType != mt) {
        d->m_mimeType = mt;
        emit mimeTypeChanged();
    }
136 137
}

Jarek Kobus's avatar
Jarek Kobus committed
138 139 140 141 142
void BaseTextDocument::setTypingSettings(const TypingSettings &typingSettings)
{
    d->m_typingSettings = typingSettings;
}

143 144 145 146 147
void BaseTextDocument::setStorageSettings(const StorageSettings &storageSettings)
{
    d->m_storageSettings = storageSettings;
}

Jarek Kobus's avatar
Jarek Kobus committed
148 149 150 151 152
const TypingSettings &BaseTextDocument::typingSettings() const
{
    return d->m_typingSettings;
}

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
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;
}

168 169 170 171 172
void BaseTextDocument::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings)
{
    d->m_extraEncodingSettings = extraEncodingSettings;
}

173 174 175 176 177 178 179 180 181 182
void BaseTextDocument::autoIndent(const QTextCursor &cursor, QChar typedChar)
{
    d->m_indenter->indent(d->m_document, cursor, typedChar, d->m_tabSettings);
}

void BaseTextDocument::autoReindent(const QTextCursor &cursor)
{
    d->m_indenter->reindent(d->m_document, cursor, d->m_tabSettings);
}

183 184 185 186 187
const ExtraEncodingSettings &BaseTextDocument::extraEncodingSettings() const
{
    return d->m_extraEncodingSettings;
}

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
void BaseTextDocument::setIndenter(Indenter *indenter)
{
    // clear out existing code formatter data
    for (QTextBlock it = document()->begin(); it.isValid(); it = it.next()) {
        TextEditor::TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(it);
        if (userData)
            userData->setCodeFormatterData(0);
    }
    d->m_indenter.reset(indenter);
}

Indenter *BaseTextDocument::indenter() const
{
    return d->m_indenter.data();
}

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
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;
}

239
ITextMarkable *BaseTextDocument::markableInterface() const
240
{
241 242
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout *>(d->m_document->documentLayout());
243
    QTC_ASSERT(documentLayout, return 0);
244
    return documentLayout->markableInterface();
con's avatar
con committed
245 246
}

247
/*!
248 249 250 251
 * 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
252
 * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()).
253
 */
254
bool BaseTextDocument::save(QString *errorString, const QString &saveFileName, bool autoSave)
con's avatar
con committed
255
{
256
    QTextCursor cursor(d->m_document);
con's avatar
con committed
257

258 259
    // When autosaving, we don't want to modify the document/location under the user's fingers.
    BaseTextEditorWidget *editorWidget = 0;
260 261
    int savedPosition = 0;
    int savedAnchor = 0;
262 263
    int savedVScrollBarValue = 0;
    int savedHScrollBarValue = 0;
264 265
    int undos = d->m_document->availableUndoSteps();

266 267
    // When saving the current editor, make sure to maintain the cursor and scroll bar
    // positions for undo
268
    IEditor *currentEditor = EditorManager::currentEditor();
269
    if (BaseTextEditor *editable = qobject_cast<BaseTextEditor*>(currentEditor)) {
270
        if (editable->document() == this) {
271 272 273 274
            editorWidget = editable->editorWidget();
            QTextCursor cur = editorWidget->textCursor();
            savedPosition = cur.position();
            savedAnchor = cur.anchor();
275 276
            savedVScrollBarValue = editorWidget->verticalScrollBar()->value();
            savedHScrollBarValue = editorWidget->horizontalScrollBar()->value();
277 278
            cursor.setPosition(cur.position());
        }
279 280
    }

281 282 283
    if (!autoSave) {
        cursor.beginEditBlock();
        cursor.movePosition(QTextCursor::Start);
284

285 286 287 288 289 290
        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
291

292
    QString fName = filePath();
293 294
    if (!saveFileName.isEmpty())
        fName = saveFileName;
con's avatar
con committed
295

296
    // check if UTF8-BOM has to be added or removed
297
    Utils::TextFileFormat saveFormat = format();
298
    if (saveFormat.codec->name() == "UTF-8" && supportsUtf8Bom()) {
299 300 301 302 303 304 305 306 307
        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;
308
        }
309
    }
con's avatar
con committed
310

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

313
    // restore text cursor and scroll bar positions
314 315 316 317 318 319
    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);
320 321
            editorWidget->verticalScrollBar()->setValue(savedVScrollBarValue);
            editorWidget->horizontalScrollBar()->setValue(savedHScrollBarValue);
322 323 324 325
            editorWidget->setTextCursor(cur);
        }
    }

326
    if (!ok)
con's avatar
con committed
327
        return false;
328 329 330
    d->m_autoSaveRevision = d->m_document->revision();
    if (autoSave)
        return true;
con's avatar
con committed
331

332
    // inform about the new filename
con's avatar
con committed
333
    const QFileInfo fi(fName);
334
    d->m_document->setModified(false);
335
    setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
con's avatar
con committed
336 337 338 339
    emit changed();
    return true;
}

340 341
bool BaseTextDocument::setContents(const QByteArray &contents)
{
342
    if (contents.size() > EditorManager::maxTextFileSize()) {
343 344 345 346 347 348 349 350 351
        document()->setPlainText(BaseTextEditorWidget::msgTextTooLarge(contents.size()));
        document()->setModified(false);
        return false;
    }
    document()->setPlainText(QString::fromUtf8(contents));
    document()->setModified(false);
    return true;
}

352 353 354 355 356
bool BaseTextDocument::shouldAutoSave() const
{
    return d->m_autoSaveRevision != d->m_document->revision();
}

357
void BaseTextDocument::setFilePath(const QString &newName)
dt's avatar
dt committed
358
{
359
    if (newName == filePath())
360
        return;
dt's avatar
dt committed
361
    const QFileInfo fi(newName);
362
    IDocument::setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
dt's avatar
dt committed
363 364
}

365
bool BaseTextDocument::isFileReadOnly() const
con's avatar
con committed
366
{
367
    if (filePath().isEmpty()) //have no corresponding file, so editing is ok
con's avatar
con committed
368
        return false;
369
    return d->m_fileIsReadOnly;
370 371 372 373
}

bool BaseTextDocument::isModified() const
{
374
    return d->m_document->isModified();
375 376 377 378
}

void BaseTextDocument::checkPermissions()
{
379
    bool previousReadOnly = d->m_fileIsReadOnly;
380 381
    if (!filePath().isEmpty()) {
        const QFileInfo fi(filePath());
382
        d->m_fileIsReadOnly = !fi.isWritable();
383
    } else {
384
        d->m_fileIsReadOnly = false;
385
    }
386
    if (previousReadOnly != d->m_fileIsReadOnly)
387
        emit changed();
con's avatar
con committed
388 389
}

390
bool BaseTextDocument::open(QString *errorString, const QString &fileName, const QString &realFileName)
con's avatar
con committed
391
{
392 393 394 395
    QStringList content;

    ReadResult readResult = Utils::TextFileFormat::ReadIOError;

con's avatar
con committed
396 397
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
398
        d->m_fileIsReadOnly = !fi.isWritable();
399
        readResult = read(realFileName, &content, errorString);
con's avatar
con committed
400

401
        d->m_document->setModified(false);
402
        const int chunks = content.size();
403 404 405
        if (chunks == 0) {
            d->m_document->setPlainText(QString());
        } else if (chunks == 1) {
406
            d->m_document->setPlainText(content.at(0));
407
        } else {
408 409
            QFutureInterface<void> interface;
            interface.setProgressRange(0, chunks);
410
            ProgressManager::addTask(interface.future(), tr("Opening file"), Constants::TASK_OPEN_FILE);
411
            interface.reportStarted();
412
            d->m_document->setUndoRedoEnabled(false);
413
            QTextCursor c(d->m_document);
414 415
            c.beginEditBlock();
            d->m_document->clear();
416 417 418 419
            for (int i = 0; i < chunks; ++i) {
                c.insertText(content.at(i));
                interface.setProgressValue(i + 1);
                QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
420
            }
421
            c.endEditBlock();
422
            d->m_document->setUndoRedoEnabled(true);
423
            interface.reportFinished();
424 425 426
        }
        BaseTextDocumentLayout *documentLayout =
            qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
hjk's avatar
hjk committed
427
        QTC_ASSERT(documentLayout, return true);
428 429
        documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document->revision();
        d->m_document->setModified(fileName != realFileName);
430
        setFilePath(QDir::cleanPath(fi.absoluteFilePath()));
con's avatar
con committed
431
    }
Robert Loehning's avatar
Robert Loehning committed
432 433
    return readResult == Utils::TextFileFormat::ReadSuccess
           || readResult == Utils::TextFileFormat::ReadEncodingError;
con's avatar
con committed
434 435
}

436
bool BaseTextDocument::reload(QString *errorString, QTextCodec *codec)
con's avatar
con committed
437
{
438
    QTC_ASSERT(codec, return false);
439
    setCodec(codec);
440
    return reload(errorString);
con's avatar
con committed
441 442
}

443
bool BaseTextDocument::reload(QString *errorString)
con's avatar
con committed
444 445
{
    emit aboutToReload();
446 447
    BaseTextDocumentLayout *documentLayout =
        qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
448
    TextMarks marks;
449
    if (documentLayout)
450
        marks = documentLayout->documentClosing(); // removes text marks non-permanently
451

452
    bool success = open(errorString, filePath(), filePath());
453 454 455

    if (documentLayout)
        documentLayout->documentReloaded(marks); // readds text marks
456
    emit reloadFinished(success);
457
    return success;
con's avatar
con committed
458 459
}

460
bool BaseTextDocument::reload(QString *errorString, ReloadFlag flag, ChangeType type)
461 462
{
    if (flag == FlagIgnore)
463
        return true;
464
    if (type == TypePermissions) {
465
        checkPermissions();
466
        return true;
467
    } else {
468
        return reload(errorString);
con's avatar
con committed
469 470 471
    }
}

472
void BaseTextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter)
con's avatar
con committed
473
{
474 475 476 477 478
    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
479 480
}

481 482


483
void BaseTextDocument::cleanWhitespace(const QTextCursor &cursor)
484
{
485 486
    bool hasSelection = cursor.hasSelection();
    QTextCursor copyCursor = cursor;
mae's avatar
mae committed
487
    copyCursor.setVisualNavigation(false);
488 489 490 491 492
    copyCursor.beginEditBlock();
    cleanWhitespace(copyCursor, true, true);
    if (!hasSelection)
        ensureFinalNewLine(copyCursor);
    copyCursor.endEditBlock();
493 494
}

495
void BaseTextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument)
con's avatar
con committed
496
{
497
    BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
mae's avatar
mae committed
498
    Q_ASSERT(cursor.visualNavigation() == false);
con's avatar
con committed
499

500
    QTextBlock block = d->m_document->findBlock(cursor.selectionStart());
501 502
    QTextBlock end;
    if (cursor.hasSelection())
503
        end = d->m_document->findBlock(cursor.selectionEnd()-1).next();
504 505

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

507
        if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) {
con's avatar
con committed
508 509

            QString blockText = block.text();
510
            d->m_tabSettings.removeTrailingWhitespace(cursor, block);
511
            if (cleanIndentation && !d->m_tabSettings.isIndentationClean(block)) {
con's avatar
con committed
512
                cursor.setPosition(block.position());
513
                int firstNonSpace = d->m_tabSettings.firstNonSpace(blockText);
con's avatar
con committed
514 515 516 517
                if (firstNonSpace == blockText.length()) {
                    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                    cursor.removeSelectedText();
                } else {
518
                    int column = d->m_tabSettings.columnAt(blockText, firstNonSpace);
con's avatar
con committed
519
                    cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
520
                    QString indentationString = d->m_tabSettings.indentationString(0, column, block);
con's avatar
con committed
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
                    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"));
    }
}
541

542
} // namespace TextEditor