/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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 The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "textdocument.h" #include "extraencodingsettings.h" #include "fontsettings.h" #include "textindenter.h" #include "storagesettings.h" #include "syntaxhighlighter.h" #include "tabsettings.h" #include "textdocumentlayout.h" #include "texteditor.h" #include "texteditorconstants.h" #include "typingsettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace Utils; /*! \class TextEditor::BaseTextDocument \brief The BaseTextDocument class is the base class for QTextDocument based documents. It is the base class for documents used by implementations of the BaseTextEditor class, and contains basic functions for retrieving text content and markers (like bookmarks). Subclasses of BaseTextEditor can either use BaseTextDocument as is (and this is the default), or created subclasses of BaseTextDocument if they have special requirements. */ namespace TextEditor { class TextDocumentPrivate { public: TextDocumentPrivate() : m_indenter(new TextIndenter(&m_document)) { } QTextCursor indentOrUnindent(const QTextCursor &textCursor, bool doIndent, const TabSettings &tabSettings, bool blockSelection = false, int column = 0, int *offset = nullptr); void resetRevisions(); void updateRevisions(); public: QString m_defaultPath; QString m_suggestedFileName; TypingSettings m_typingSettings; StorageSettings m_storageSettings; TabSettings m_tabSettings; ExtraEncodingSettings m_extraEncodingSettings; FontSettings m_fontSettings; bool m_fontSettingsNeedsApply = false; // for applying font settings delayed till an editor becomes visible QTextDocument m_document; SyntaxHighlighter *m_highlighter = nullptr; CompletionAssistProvider *m_completionAssistProvider = nullptr; CompletionAssistProvider *m_functionHintAssistProvider = nullptr; IAssistProvider *m_quickFixProvider = nullptr; QScopedPointer m_indenter; QScopedPointer m_formatter; bool m_fileIsReadOnly = false; int m_autoSaveRevision = -1; TextMarks m_marksCache; // Marks not owned Utils::Guard m_modificationChangedGuard; }; QTextCursor TextDocumentPrivate::indentOrUnindent(const QTextCursor &textCursor, bool doIndent, const TabSettings &tabSettings, bool blockSelection, int columnIn, int *offset) { QTextCursor cursor = textCursor; cursor.beginEditBlock(); // Indent or unindent the selected lines int pos = cursor.position(); int column = blockSelection ? columnIn : tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock()); int anchor = cursor.anchor(); int start = qMin(anchor, pos); int end = qMax(anchor, pos); bool modified = true; QTextBlock startBlock = m_document.findBlock(start); QTextBlock endBlock = m_document.findBlock(blockSelection ? end : qMax(end - 1, 0)).next(); const bool cursorAtBlockStart = (textCursor.position() == startBlock.position()); const bool anchorAtBlockStart = (textCursor.anchor() == startBlock.position()); const bool oneLinePartial = (startBlock.next() == endBlock) && (start > startBlock.position() || end < endBlock.position() - 1); // Make sure one line selection will get processed in "for" loop if (startBlock == endBlock) endBlock = endBlock.next(); if (cursor.hasSelection() && !blockSelection && !oneLinePartial) { for (QTextBlock block = startBlock; block != endBlock; block = block.next()) { const QString text = block.text(); int indentPosition = tabSettings.lineIndentPosition(text); if (!doIndent && !indentPosition) indentPosition = tabSettings.firstNonSpace(text); int targetColumn = tabSettings.indentedColumn( tabSettings.columnAt(text, indentPosition), doIndent); cursor.setPosition(block.position() + indentPosition); cursor.insertText(tabSettings.indentationString(0, targetColumn, 0, block)); cursor.setPosition(block.position()); cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } // make sure that selection that begins in first column stays at first column // even if we insert text at first column if (cursorAtBlockStart) { cursor = textCursor; cursor.setPosition(startBlock.position(), QTextCursor::KeepAnchor); } else if (anchorAtBlockStart) { cursor = textCursor; cursor.setPosition(startBlock.position(), QTextCursor::MoveAnchor); cursor.setPosition(textCursor.position(), QTextCursor::KeepAnchor); } else { modified = false; } } else if (cursor.hasSelection() && !blockSelection && oneLinePartial) { // Only one line partially selected. cursor.removeSelectedText(); } else { // Indent or unindent at cursor position int maxTargetColumn = -1; class BlockIndenter { public: BlockIndenter(const QTextBlock &_block, const int column, const TabSettings &_tabSettings) : block(_block) , text(block.text()) , tabSettings(_tabSettings) { indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true); spaces = tabSettings.spacesLeftFromPosition(text, indentPosition); } void indent(const int targetColumn) const { const int startColumn = tabSettings.columnAt(text, indentPosition - spaces); QTextCursor cursor(block); cursor.setPosition(block.position() + indentPosition); cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.insertText(tabSettings.indentationString(startColumn, targetColumn, 0, block)); } int targetColumn(bool doIndent) const { const int optimumTargetColumn = tabSettings.indentedColumn(tabSettings.columnAt(block.text(), indentPosition), doIndent); const int minimumTargetColumn = tabSettings.columnAt(text, indentPosition - spaces); return std::max(optimumTargetColumn, minimumTargetColumn); } const QTextBlock &textBlock() { return block; } private: QTextBlock block; const QString text; int indentPosition; int spaces; const TabSettings &tabSettings; }; std::vector blocks; for (QTextBlock block = startBlock; block != endBlock; block = block.next()) { QString text = block.text(); const int blockColumn = tabSettings.columnAt(text, text.size()); if (blockColumn < column) { cursor.setPosition(block.position() + text.size()); cursor.insertText(tabSettings.indentationString(blockColumn, column, 0, block)); text = block.text(); } blocks.emplace_back(BlockIndenter(block, column, tabSettings)); maxTargetColumn = std::max(maxTargetColumn, blocks.back().targetColumn(doIndent)); } for (const BlockIndenter &blockIndenter : blocks) blockIndenter.indent(maxTargetColumn); // Preserve initial anchor of block selection if (blockSelection) { if (offset) *offset = maxTargetColumn - column; startBlock = pos < anchor ? blocks.front().textBlock() : blocks.back().textBlock(); start = startBlock.position() + tabSettings.positionAtColumn(startBlock.text(), maxTargetColumn); endBlock = pos > anchor ? blocks.front().textBlock() : blocks.back().textBlock(); end = endBlock.position() + tabSettings.positionAtColumn(endBlock.text(), maxTargetColumn); cursor.setPosition(end); cursor.setPosition(start, QTextCursor::KeepAnchor); } } cursor.endEditBlock(); return modified ? cursor : textCursor; } void TextDocumentPrivate::resetRevisions() { auto documentLayout = qobject_cast(m_document.documentLayout()); QTC_ASSERT(documentLayout, return); documentLayout->lastSaveRevision = m_document.revision(); for (QTextBlock block = m_document.begin(); block.isValid(); block = block.next()) block.setRevision(documentLayout->lastSaveRevision); } void TextDocumentPrivate::updateRevisions() { auto documentLayout = qobject_cast(m_document.documentLayout()); QTC_ASSERT(documentLayout, return); int oldLastSaveRevision = documentLayout->lastSaveRevision; documentLayout->lastSaveRevision = m_document.revision(); if (oldLastSaveRevision != documentLayout->lastSaveRevision) { for (QTextBlock block = m_document.begin(); block.isValid(); block = block.next()) { if (block.revision() < 0 || block.revision() != oldLastSaveRevision) block.setRevision(-documentLayout->lastSaveRevision - 1); else block.setRevision(documentLayout->lastSaveRevision); } } } /////////////////////////////////////////////////////////////////////// // // BaseTextDocument // /////////////////////////////////////////////////////////////////////// TextDocument::TextDocument(Id id) : d(new TextDocumentPrivate) { connect(&d->m_document, &QTextDocument::modificationChanged, this, &TextDocument::modificationChanged); connect(&d->m_document, &QTextDocument::contentsChanged, this, &Core::IDocument::contentsChanged); connect(&d->m_document, &QTextDocument::contentsChange, this, &TextDocument::contentsChangedWithPosition); // 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); d->m_document.setDocumentLayout(new TextDocumentLayout(&d->m_document)); if (id.isValid()) setId(id); setSuspendAllowed(true); } TextDocument::~TextDocument() { delete d; } QMap TextDocument::openedTextDocumentContents() { QMap workingCopy; foreach (IDocument *document, DocumentModel::openedDocuments()) { auto textEditorDocument = qobject_cast(document); if (!textEditorDocument) continue; QString fileName = textEditorDocument->filePath().toString(); workingCopy[fileName] = textEditorDocument->plainText(); } return workingCopy; } QMap TextDocument::openedTextDocumentEncodings() { QMap workingCopy; foreach (IDocument *document, DocumentModel::openedDocuments()) { auto textEditorDocument = qobject_cast(document); if (!textEditorDocument) continue; QString fileName = textEditorDocument->filePath().toString(); workingCopy[fileName] = const_cast(textEditorDocument->codec()); } return workingCopy; } TextDocument *TextDocument::currentTextDocument() { return qobject_cast(EditorManager::currentDocument()); } TextDocument *TextDocument::textDocumentForFilePath(const Utils::FilePath &filePath) { return qobject_cast(DocumentModel::documentForFilePath(filePath)); } QString TextDocument::plainText() const { return document()->toPlainText(); } QString TextDocument::textAt(int pos, int length) const { return Utils::Text::textAt(QTextCursor(document()), pos, length); } QChar TextDocument::characterAt(int pos) const { return document()->characterAt(pos); } void TextDocument::setTypingSettings(const TypingSettings &typingSettings) { d->m_typingSettings = typingSettings; } void TextDocument::setStorageSettings(const StorageSettings &storageSettings) { d->m_storageSettings = storageSettings; } const TypingSettings &TextDocument::typingSettings() const { return d->m_typingSettings; } const StorageSettings &TextDocument::storageSettings() const { return d->m_storageSettings; } void TextDocument::setTabSettings(const TabSettings &newTabSettings) { if (newTabSettings == d->m_tabSettings) return; d->m_tabSettings = newTabSettings; emit tabSettingsChanged(); } TabSettings TextDocument::tabSettings() const { return d->m_tabSettings; } void TextDocument::setFontSettings(const FontSettings &fontSettings) { if (fontSettings == d->m_fontSettings) return; d->m_fontSettings = fontSettings; d->m_fontSettingsNeedsApply = true; emit fontSettingsChanged(); } QAction *TextDocument::createDiffAgainstCurrentFileAction( QObject *parent, const std::function &filePath) { const auto diffAgainstCurrentFile = [filePath]() { auto diffService = DiffService::instance(); auto textDocument = TextEditor::TextDocument::currentTextDocument(); const QString leftFilePath = textDocument ? textDocument->filePath().toString() : QString(); const QString rightFilePath = filePath().toString(); if (diffService && !leftFilePath.isEmpty() && !rightFilePath.isEmpty()) diffService->diffFiles(leftFilePath, rightFilePath); }; auto diffAction = new QAction(tr("Diff Against Current File"), parent); QObject::connect(diffAction, &QAction::triggered, parent, diffAgainstCurrentFile); return diffAction; } void TextDocument::triggerPendingUpdates() { if (d->m_fontSettingsNeedsApply) applyFontSettings(); } void TextDocument::setCompletionAssistProvider(CompletionAssistProvider *provider) { d->m_completionAssistProvider = provider; } CompletionAssistProvider *TextDocument::completionAssistProvider() const { return d->m_completionAssistProvider; } void TextDocument::setFunctionHintAssistProvider(CompletionAssistProvider *provider) { d->m_functionHintAssistProvider = provider; } CompletionAssistProvider *TextDocument::functionHintAssistProvider() const { return d->m_functionHintAssistProvider; } void TextDocument::setQuickFixAssistProvider(IAssistProvider *provider) const { d->m_quickFixProvider = provider; } IAssistProvider *TextDocument::quickFixAssistProvider() const { return d->m_quickFixProvider; } void TextDocument::applyFontSettings() { d->m_fontSettingsNeedsApply = false; if (d->m_highlighter) { d->m_highlighter->setFontSettings(d->m_fontSettings); d->m_highlighter->rehighlight(); } } const FontSettings &TextDocument::fontSettings() const { return d->m_fontSettings; } void TextDocument::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings) { d->m_extraEncodingSettings = extraEncodingSettings; } void TextDocument::autoIndent(const QTextCursor &cursor, QChar typedChar, int currentCursorPosition) { d->m_indenter->indent(cursor, typedChar, tabSettings(), currentCursorPosition); } void TextDocument::autoReindent(const QTextCursor &cursor, int currentCursorPosition) { d->m_indenter->reindent(cursor, tabSettings(), currentCursorPosition); } void TextDocument::autoFormatOrIndent(const QTextCursor &cursor) { d->m_indenter->autoIndent(cursor, tabSettings()); } QTextCursor TextDocument::indent(const QTextCursor &cursor, bool blockSelection, int column, int *offset) { return d->indentOrUnindent(cursor, true, tabSettings(), blockSelection, column, offset); } QTextCursor TextDocument::unindent(const QTextCursor &cursor, bool blockSelection, int column, int *offset) { return d->indentOrUnindent(cursor, false, tabSettings(), blockSelection, column, offset); } void TextDocument::setFormatter(Formatter *formatter) { d->m_formatter.reset(formatter); } void TextDocument::autoFormat(const QTextCursor &cursor) { using namespace Utils::Text; if (!d->m_formatter) return; if (QFutureWatcher *watcher = d->m_formatter->format(cursor, tabSettings())) { connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { if (!watcher->isCanceled()) Utils::Text::applyReplacements(document(), watcher->result()); delete watcher; }); } } const ExtraEncodingSettings &TextDocument::extraEncodingSettings() const { return d->m_extraEncodingSettings; } void TextDocument::setIndenter(Indenter *indenter) { // clear out existing code formatter data for (QTextBlock it = document()->begin(); it.isValid(); it = it.next()) { TextBlockUserData *userData = TextDocumentLayout::textUserData(it); if (userData) userData->setCodeFormatterData(nullptr); } d->m_indenter.reset(indenter); } Indenter *TextDocument::indenter() const { return d->m_indenter.data(); } bool TextDocument::isSaveAsAllowed() const { return true; } QString TextDocument::fallbackSaveAsPath() const { return d->m_defaultPath; } QString TextDocument::fallbackSaveAsFileName() const { return d->m_suggestedFileName; } void TextDocument::setFallbackSaveAsPath(const QString &defaultPath) { d->m_defaultPath = defaultPath; } void TextDocument::setFallbackSaveAsFileName(const QString &suggestedFileName) { d->m_suggestedFileName = suggestedFileName; } QTextDocument *TextDocument::document() const { return &d->m_document; } SyntaxHighlighter *TextDocument::syntaxHighlighter() const { return d->m_highlighter; } /*! * 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 * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()). */ bool TextDocument::save(QString *errorString, const QString &saveFileName, bool autoSave) { QTextCursor cursor(&d->m_document); // When autosaving, we don't want to modify the document/location under the user's fingers. TextEditorWidget *editorWidget = nullptr; int savedPosition = 0; int savedAnchor = 0; int savedVScrollBarValue = 0; int savedHScrollBarValue = 0; int undos = d->m_document.availableUndoSteps(); // When saving the current editor, make sure to maintain the cursor and scroll bar // positions for undo if (BaseTextEditor *editor = BaseTextEditor::currentTextEditor()) { if (editor->document() == this) { editorWidget = editor->editorWidget(); QTextCursor cur = editor->textCursor(); savedPosition = cur.position(); savedAnchor = cur.anchor(); savedVScrollBarValue = editorWidget->verticalScrollBar()->value(); savedHScrollBarValue = editorWidget->horizontalScrollBar()->value(); cursor.setPosition(cur.position()); } } if (!autoSave) { cursor.beginEditBlock(); cursor.movePosition(QTextCursor::Start); if (d->m_storageSettings.m_cleanWhitespace) { cleanWhitespace(cursor, d->m_storageSettings.m_inEntireDocument, d->m_storageSettings.m_cleanIndentation); } if (d->m_storageSettings.m_addFinalNewLine) ensureFinalNewLine(cursor); cursor.endEditBlock(); } QString fName = filePath().toString(); if (!saveFileName.isEmpty()) fName = saveFileName; // check if UTF8-BOM has to be added or removed Utils::TextFileFormat saveFormat = format(); if (saveFormat.codec->name() == "UTF-8" && supportsUtf8Bom()) { switch (d->m_extraEncodingSettings.m_utf8BomSetting) { case ExtraEncodingSettings::AlwaysAdd: saveFormat.hasUtf8Bom = true; break; case ExtraEncodingSettings::OnlyKeep: break; case ExtraEncodingSettings::AlwaysDelete: saveFormat.hasUtf8Bom = false; break; } } const bool ok = write(fName, saveFormat, d->m_document.toPlainText(), errorString); // restore text cursor and scroll bar positions 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); editorWidget->verticalScrollBar()->setValue(savedVScrollBarValue); editorWidget->horizontalScrollBar()->setValue(savedHScrollBarValue); editorWidget->setTextCursor(cur); } } if (!ok) return false; d->m_autoSaveRevision = d->m_document.revision(); if (autoSave) return true; // inform about the new filename const QFileInfo fi(fName); d->m_document.setModified(false); // also triggers update of the block revisions setFilePath(Utils::FilePath::fromUserInput(fi.absoluteFilePath())); emit changed(); return true; } QByteArray TextDocument::contents() const { return plainText().toUtf8(); } bool TextDocument::setContents(const QByteArray &contents) { return setPlainText(QString::fromUtf8(contents)); } bool TextDocument::shouldAutoSave() const { return d->m_autoSaveRevision != d->m_document.revision(); } void TextDocument::setFilePath(const Utils::FilePath &newName) { if (newName == filePath()) return; IDocument::setFilePath(Utils::FilePath::fromUserInput(newName.toFileInfo().absoluteFilePath())); } bool TextDocument::isFileReadOnly() const { if (filePath().isEmpty()) //have no corresponding file, so editing is ok return false; return d->m_fileIsReadOnly; } bool TextDocument::isModified() const { return d->m_document.isModified(); } void TextDocument::checkPermissions() { bool previousReadOnly = d->m_fileIsReadOnly; if (!filePath().isEmpty()) { d->m_fileIsReadOnly = !filePath().toFileInfo().isWritable(); } else { d->m_fileIsReadOnly = false; } if (previousReadOnly != d->m_fileIsReadOnly) emit changed(); } Core::IDocument::OpenResult TextDocument::open(QString *errorString, const QString &fileName, const QString &realFileName) { emit aboutToOpen(fileName, realFileName); OpenResult success = openImpl(errorString, fileName, realFileName, /*reload =*/ false); if (success == OpenResult::Success) { setMimeType(Utils::mimeTypeForFile(fileName).name()); emit openFinishedSuccessfully(); } return success; } Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, const QString &fileName, const QString &realFileName, bool reload) { QStringList content; ReadResult readResult = Utils::TextFileFormat::ReadIOError; if (!fileName.isEmpty()) { const QFileInfo fi(fileName); d->m_fileIsReadOnly = !fi.isWritable(); readResult = read(realFileName, &content, errorString); const int chunks = content.size(); // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different, // since it will reset the undo's clear index if (!reload || fileName == realFileName) d->m_document.setUndoRedoEnabled(reload); QTextCursor c(&d->m_document); c.beginEditBlock(); if (reload) { c.select(QTextCursor::Document); c.removeSelectedText(); } else { d->m_document.clear(); } if (chunks == 1) { c.insertText(content.at(0)); } else if (chunks > 1) { QFutureInterface interface; interface.setProgressRange(0, chunks); ProgressManager::addTask(interface.future(), tr("Opening File"), Constants::TASK_OPEN_FILE); interface.reportStarted(); for (int i = 0; i < chunks; ++i) { c.insertText(content.at(i)); interface.setProgressValue(i + 1); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } interface.reportFinished(); } c.endEditBlock(); // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different, // since it will reset the undo's clear index if (!reload || fileName == realFileName) d->m_document.setUndoRedoEnabled(true); auto documentLayout = qobject_cast(d->m_document.documentLayout()); QTC_ASSERT(documentLayout, return OpenResult::CannotHandle); documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document.revision(); d->updateRevisions(); d->m_document.setModified(fileName != realFileName); setFilePath(Utils::FilePath::fromUserInput(fi.absoluteFilePath())); } if (readResult == Utils::TextFileFormat::ReadIOError) return OpenResult::ReadError; return OpenResult::Success; } bool TextDocument::reload(QString *errorString, QTextCodec *codec) { QTC_ASSERT(codec, return false); setCodec(codec); return reload(errorString); } bool TextDocument::reload(QString *errorString) { return reload(errorString, filePath().toString()); } bool TextDocument::reload(QString *errorString, const QString &realFileName) { emit aboutToReload(); auto documentLayout = qobject_cast(d->m_document.documentLayout()); TextMarks marks; if (documentLayout) marks = documentLayout->documentClosing(); // removes text marks non-permanently const QString &file = filePath().toString(); bool success = openImpl(errorString, file, realFileName, /*reload =*/ true) == OpenResult::Success; if (documentLayout) documentLayout->documentReloaded(marks, this); // re-adds text marks emit reloadFinished(success); return success; } bool TextDocument::setPlainText(const QString &text) { if (text.size() > EditorManager::maxTextFileSize()) { document()->setPlainText(TextEditorWidget::msgTextTooLarge(text.size())); d->resetRevisions(); document()->setModified(false); return false; } document()->setPlainText(text); d->resetRevisions(); document()->setModified(false); return true; } bool TextDocument::reload(QString *errorString, ReloadFlag flag, ChangeType type) { if (flag == FlagIgnore) { if (type != TypeContents) return true; const bool wasModified = document()->isModified(); { Utils::GuardLocker locker(d->m_modificationChangedGuard); // hack to ensure we clean the clear state in QTextDocument document()->setModified(false); document()->setModified(true); } if (!wasModified) modificationChanged(true); return true; } if (type == TypePermissions) { checkPermissions(); return true; } else { return reload(errorString); } } void TextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter) { if (d->m_highlighter) delete d->m_highlighter; d->m_highlighter = highlighter; d->m_highlighter->setParent(this); d->m_highlighter->setDocument(&d->m_document); } void TextDocument::cleanWhitespace(const QTextCursor &cursor) { bool hasSelection = cursor.hasSelection(); QTextCursor copyCursor = cursor; copyCursor.setVisualNavigation(false); copyCursor.beginEditBlock(); cleanWhitespace(copyCursor, true, true); if (!hasSelection) ensureFinalNewLine(copyCursor); copyCursor.endEditBlock(); } void TextDocument::cleanWhitespace(QTextCursor &cursor, bool inEntireDocument, bool cleanIndentation) { const QString fileName(filePath().fileName()); auto documentLayout = qobject_cast(d->m_document.documentLayout()); Q_ASSERT(cursor.visualNavigation() == false); QTextBlock block = d->m_document.findBlock(cursor.selectionStart()); QTextBlock end; if (cursor.hasSelection()) end = d->m_document.findBlock(cursor.selectionEnd()-1).next(); QVector blocks; while (block.isValid() && block != end) { if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) { blocks.append(block); } block = block.next(); } if (blocks.isEmpty()) return; const TabSettings currentTabSettings = tabSettings(); const IndentationForBlock &indentations = d->m_indenter->indentationForBlocks(blocks, currentTabSettings); foreach (block, blocks) { QString blockText = block.text(); if (d->m_storageSettings.removeTrailingWhitespace(fileName)) currentTabSettings.removeTrailingWhitespace(cursor, block); const int indent = indentations[block.blockNumber()]; if (cleanIndentation && !currentTabSettings.isIndentationClean(block, indent)) { cursor.setPosition(block.position()); int firstNonSpace = currentTabSettings.firstNonSpace(blockText); if (firstNonSpace == blockText.length()) { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } else { int column = currentTabSettings.columnAt(blockText, firstNonSpace); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace); QString indentationString = currentTabSettings.indentationString(0, column, column - indent, block); cursor.insertText(indentationString); } } } } void TextDocument::ensureFinalNewLine(QTextCursor& cursor) { if (!d->m_storageSettings.m_addFinalNewLine) return; 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")); } } void TextDocument::modificationChanged(bool modified) { if (d->m_modificationChangedGuard.isLocked()) return; // we only want to update the block revisions when going back to the saved version, // e.g. with undo if (!modified) d->updateRevisions(); emit changed(); } void TextDocument::updateLayout() const { auto documentLayout = qobject_cast(d->m_document.documentLayout()); QTC_ASSERT(documentLayout, return); documentLayout->requestUpdate(); } TextMarks TextDocument::marks() const { return d->m_marksCache; } bool TextDocument::addMark(TextMark *mark) { if (mark->baseTextDocument()) return false; QTC_ASSERT(mark->lineNumber() >= 1, return false); int blockNumber = mark->lineNumber() - 1; auto documentLayout = qobject_cast(d->m_document.documentLayout()); QTC_ASSERT(documentLayout, return false); QTextBlock block = d->m_document.findBlockByNumber(blockNumber); if (block.isValid()) { TextBlockUserData *userData = TextDocumentLayout::userData(block); userData->addMark(mark); d->m_marksCache.append(mark); mark->updateLineNumber(blockNumber + 1); QTC_CHECK(mark->lineNumber() == blockNumber + 1); // Checks that the base class is called mark->updateBlock(block); mark->setBaseTextDocument(this); if (!mark->isVisible()) return true; // Update document layout double newMaxWidthFactor = qMax(mark->widthFactor(), documentLayout->maxMarkWidthFactor); bool fullUpdate = newMaxWidthFactor > documentLayout->maxMarkWidthFactor || !documentLayout->hasMarks; documentLayout->hasMarks = true; documentLayout->maxMarkWidthFactor = newMaxWidthFactor; if (fullUpdate) documentLayout->requestUpdate(); else documentLayout->requestExtraAreaUpdate(); return true; } return false; } TextMarks TextDocument::marksAt(int line) const { QTC_ASSERT(line >= 1, return TextMarks()); int blockNumber = line - 1; QTextBlock block = d->m_document.findBlockByNumber(blockNumber); if (block.isValid()) { if (TextBlockUserData *userData = TextDocumentLayout::textUserData(block)) return userData->marks(); } return TextMarks(); } void TextDocument::removeMarkFromMarksCache(TextMark *mark) { auto documentLayout = qobject_cast(d->m_document.documentLayout()); QTC_ASSERT(documentLayout, return); d->m_marksCache.removeAll(mark); auto scheduleLayoutUpdate = [documentLayout](){ // make sure all destructors that may directly or indirectly call this function are // completed before updating. QTimer::singleShot(0, documentLayout, &QPlainTextDocumentLayout::requestUpdate); }; if (d->m_marksCache.isEmpty()) { documentLayout->hasMarks = false; documentLayout->maxMarkWidthFactor = 1.0; scheduleLayoutUpdate(); return; } if (!mark->isVisible()) return; if (documentLayout->maxMarkWidthFactor == 1.0 || mark->widthFactor() == 1.0 || mark->widthFactor() < documentLayout->maxMarkWidthFactor) { // No change in width possible documentLayout->requestExtraAreaUpdate(); } else { double maxWidthFactor = 1.0; foreach (const TextMark *mark, marks()) { if (!mark->isVisible()) continue; maxWidthFactor = qMax(mark->widthFactor(), maxWidthFactor); if (maxWidthFactor == documentLayout->maxMarkWidthFactor) break; // Still a mark with the maxMarkWidthFactor } if (maxWidthFactor != documentLayout->maxMarkWidthFactor) { documentLayout->maxMarkWidthFactor = maxWidthFactor; scheduleLayoutUpdate(); } else { documentLayout->requestExtraAreaUpdate(); } } } void TextDocument::removeMark(TextMark *mark) { QTextBlock block = d->m_document.findBlockByNumber(mark->lineNumber() - 1); if (auto data = static_cast(block.userData())) { if (!data->removeMark(mark)) qDebug() << "Could not find mark" << mark << "on line" << mark->lineNumber(); } removeMarkFromMarksCache(mark); emit markRemoved(mark); mark->setBaseTextDocument(nullptr); updateLayout(); } void TextDocument::updateMark(TextMark *mark) { QTextBlock block = d->m_document.findBlockByNumber(mark->lineNumber() - 1); if (block.isValid()) { TextBlockUserData *userData = TextDocumentLayout::userData(block); // re-evaluate priority userData->removeMark(mark); userData->addMark(mark); } updateLayout(); } void TextDocument::moveMark(TextMark *mark, int previousLine) { QTextBlock block = d->m_document.findBlockByNumber(previousLine - 1); if (TextBlockUserData *data = TextDocumentLayout::textUserData(block)) { if (!data->removeMark(mark)) qDebug() << "Could not find mark" << mark << "on line" << previousLine; } removeMarkFromMarksCache(mark); mark->setBaseTextDocument(nullptr); addMark(mark); } } // namespace TextEditor