1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "textdocument.h"
27 
28 #include "extraencodingsettings.h"
29 #include "fontsettings.h"
30 #include "textindenter.h"
31 #include "storagesettings.h"
32 #include "syntaxhighlighter.h"
33 #include "tabsettings.h"
34 #include "textdocumentlayout.h"
35 #include "texteditor.h"
36 #include "texteditorconstants.h"
37 #include "typingsettings.h"
38 #include <coreplugin/diffservice.h>
39 #include <coreplugin/editormanager/editormanager.h>
40 #include <coreplugin/editormanager/documentmodel.h>
41 #include <extensionsystem/pluginmanager.h>
42 #include <utils/textutils.h>
43 #include <utils/guard.h>
44 #include <utils/mimetypes/mimedatabase.h>
45 
46 #include <QAction>
47 #include <QApplication>
48 #include <QDir>
49 #include <QFileInfo>
50 #include <QFutureInterface>
51 #include <QScrollBar>
52 #include <QStringList>
53 #include <QTextCodec>
54 
55 #include <coreplugin/coreconstants.h>
56 #include <coreplugin/icore.h>
57 #include <coreplugin/progressmanager/progressmanager.h>
58 #include <utils/qtcassert.h>
59 
60 using namespace Core;
61 using namespace Utils;
62 
63 /*!
64     \class TextEditor::BaseTextDocument
65     \brief The BaseTextDocument class is the base class for QTextDocument based documents.
66 
67     It is the base class for documents used by implementations of the BaseTextEditor class,
68     and contains basic functions for retrieving text content and markers (like bookmarks).
69 
70     Subclasses of BaseTextEditor can either use BaseTextDocument as is (and this is the default),
71     or created subclasses of BaseTextDocument if they have special requirements.
72 */
73 
74 namespace TextEditor {
75 
76 class TextDocumentPrivate
77 {
78 public:
TextDocumentPrivate()79     TextDocumentPrivate()
80         : m_indenter(new TextIndenter(&m_document))
81     {
82     }
83 
84     QTextCursor indentOrUnindent(const QTextCursor &textCursor, bool doIndent,
85                                  const TabSettings &tabSettings,
86                                  bool blockSelection = false, int column = 0,
87                                  int *offset = nullptr);
88     void resetRevisions();
89     void updateRevisions();
90 
91 public:
92     QString m_defaultPath;
93     QString m_suggestedFileName;
94     TypingSettings m_typingSettings;
95     StorageSettings m_storageSettings;
96     TabSettings m_tabSettings;
97     ExtraEncodingSettings m_extraEncodingSettings;
98     FontSettings m_fontSettings;
99     bool m_fontSettingsNeedsApply = false; // for applying font settings delayed till an editor becomes visible
100     QTextDocument m_document;
101     SyntaxHighlighter *m_highlighter = nullptr;
102     CompletionAssistProvider *m_completionAssistProvider = nullptr;
103     CompletionAssistProvider *m_functionHintAssistProvider = nullptr;
104     IAssistProvider *m_quickFixProvider = nullptr;
105     QScopedPointer<Indenter> m_indenter;
106     QScopedPointer<Formatter> m_formatter;
107 
108     int m_autoSaveRevision = -1;
109 
110     TextMarks m_marksCache; // Marks not owned
111     Utils::Guard m_modificationChangedGuard;
112 };
113 
indentOrUnindent(const QTextCursor & textCursor,bool doIndent,const TabSettings & tabSettings,bool blockSelection,int columnIn,int * offset)114 QTextCursor TextDocumentPrivate::indentOrUnindent(const QTextCursor &textCursor, bool doIndent,
115                                                   const TabSettings &tabSettings,
116                                                   bool blockSelection, int columnIn, int *offset)
117 {
118     QTextCursor cursor = textCursor;
119     cursor.beginEditBlock();
120 
121     // Indent or unindent the selected lines
122     int pos = cursor.position();
123     int column = blockSelection ? columnIn
124                : tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock());
125     int anchor = cursor.anchor();
126     int start = qMin(anchor, pos);
127     int end = qMax(anchor, pos);
128     bool modified = true;
129 
130     QTextBlock startBlock = m_document.findBlock(start);
131     QTextBlock endBlock = m_document.findBlock(blockSelection ? end : qMax(end - 1, 0)).next();
132     const bool cursorAtBlockStart = (textCursor.position() == startBlock.position());
133     const bool anchorAtBlockStart = (textCursor.anchor() == startBlock.position());
134     const bool oneLinePartial = (startBlock.next() == endBlock)
135                               && (start > startBlock.position() || end < endBlock.position() - 1);
136 
137     // Make sure one line selection will get processed in "for" loop
138     if (startBlock == endBlock)
139         endBlock = endBlock.next();
140 
141     if (cursor.hasSelection() && !blockSelection && !oneLinePartial) {
142         for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
143             const QString text = block.text();
144             int indentPosition = tabSettings.lineIndentPosition(text);
145             if (!doIndent && !indentPosition)
146                 indentPosition = TabSettings::firstNonSpace(text);
147             int targetColumn = tabSettings.indentedColumn(
148                         tabSettings.columnAt(text, indentPosition), doIndent);
149             cursor.setPosition(block.position() + indentPosition);
150             cursor.insertText(tabSettings.indentationString(0, targetColumn, 0, block));
151             cursor.setPosition(block.position());
152             cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor);
153             cursor.removeSelectedText();
154         }
155         // make sure that selection that begins in first column stays at first column
156         // even if we insert text at first column
157         if (cursorAtBlockStart) {
158             cursor = textCursor;
159             cursor.setPosition(startBlock.position(), QTextCursor::KeepAnchor);
160         } else if (anchorAtBlockStart) {
161             cursor = textCursor;
162             cursor.setPosition(startBlock.position(), QTextCursor::MoveAnchor);
163             cursor.setPosition(textCursor.position(), QTextCursor::KeepAnchor);
164         } else {
165             modified = false;
166         }
167     } else if (cursor.hasSelection() && !blockSelection && oneLinePartial) {
168         // Only one line partially selected.
169         cursor.removeSelectedText();
170     } else {
171         // Indent or unindent at cursor position
172         int maxTargetColumn = -1;
173 
174         class BlockIndenter
175         {
176         public:
177             BlockIndenter(const QTextBlock &_block,
178                           const int column,
179                           const TabSettings &_tabSettings)
180                 : block(_block)
181                 , text(block.text())
182                 , tabSettings(_tabSettings)
183             {
184                 indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true);
185                 spaces = TabSettings::spacesLeftFromPosition(text, indentPosition);
186             }
187 
188             void indent(const int targetColumn) const
189             {
190                 const int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
191                 QTextCursor cursor(block);
192                 cursor.setPosition(block.position() + indentPosition);
193                 cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor);
194                 cursor.removeSelectedText();
195                 cursor.insertText(tabSettings.indentationString(startColumn, targetColumn, 0, block));
196             }
197 
198             int targetColumn(bool doIndent) const
199             {
200                 const int optimumTargetColumn
201                     = tabSettings.indentedColumn(tabSettings.columnAt(block.text(), indentPosition),
202                                                  doIndent);
203                 const int minimumTargetColumn = tabSettings.columnAt(text, indentPosition - spaces);
204                 return std::max(optimumTargetColumn, minimumTargetColumn);
205             }
206 
207             const QTextBlock &textBlock() { return block; }
208 
209         private:
210             QTextBlock block;
211             const QString text;
212             int indentPosition;
213             int spaces;
214             const TabSettings &tabSettings;
215         };
216 
217         std::vector<BlockIndenter> blocks;
218 
219         for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
220             QString text = block.text();
221 
222             const int blockColumn = tabSettings.columnAt(text, text.size());
223             if (blockColumn < column) {
224                 cursor.setPosition(block.position() + text.size());
225                 cursor.insertText(tabSettings.indentationString(blockColumn, column, 0, block));
226                 text = block.text();
227             }
228 
229             blocks.emplace_back(BlockIndenter(block, column, tabSettings));
230             maxTargetColumn = std::max(maxTargetColumn, blocks.back().targetColumn(doIndent));
231         }
232         for (const BlockIndenter &blockIndenter : blocks)
233             blockIndenter.indent(maxTargetColumn);
234 
235         // Preserve initial anchor of block selection
236         if (blockSelection) {
237             if (offset)
238                 *offset = maxTargetColumn - column;
239             startBlock = pos < anchor ? blocks.front().textBlock() : blocks.back().textBlock();
240             start = startBlock.position()
241                     + tabSettings.positionAtColumn(startBlock.text(), maxTargetColumn);
242             endBlock = pos > anchor ? blocks.front().textBlock() : blocks.back().textBlock();
243             end = endBlock.position()
244                   + tabSettings.positionAtColumn(endBlock.text(), maxTargetColumn);
245 
246             cursor.setPosition(end);
247             cursor.setPosition(start, QTextCursor::KeepAnchor);
248         }
249     }
250 
251     cursor.endEditBlock();
252 
253     return modified ? cursor : textCursor;
254 }
255 
resetRevisions()256 void TextDocumentPrivate::resetRevisions()
257 {
258     auto documentLayout = qobject_cast<TextDocumentLayout*>(m_document.documentLayout());
259     QTC_ASSERT(documentLayout, return);
260     documentLayout->lastSaveRevision = m_document.revision();
261 
262     for (QTextBlock block = m_document.begin(); block.isValid(); block = block.next())
263         block.setRevision(documentLayout->lastSaveRevision);
264 }
265 
updateRevisions()266 void TextDocumentPrivate::updateRevisions()
267 {
268     auto documentLayout = qobject_cast<TextDocumentLayout*>(m_document.documentLayout());
269     QTC_ASSERT(documentLayout, return);
270     int oldLastSaveRevision = documentLayout->lastSaveRevision;
271     documentLayout->lastSaveRevision = m_document.revision();
272 
273     if (oldLastSaveRevision != documentLayout->lastSaveRevision) {
274         for (QTextBlock block = m_document.begin(); block.isValid(); block = block.next()) {
275             if (block.revision() < 0 || block.revision() != oldLastSaveRevision)
276                 block.setRevision(-documentLayout->lastSaveRevision - 1);
277             else
278                 block.setRevision(documentLayout->lastSaveRevision);
279         }
280     }
281 }
282 
283 ///////////////////////////////////////////////////////////////////////
284 //
285 // BaseTextDocument
286 //
287 ///////////////////////////////////////////////////////////////////////
288 
TextDocument(Id id)289 TextDocument::TextDocument(Id id)
290     : d(new TextDocumentPrivate)
291 {
292     connect(&d->m_document, &QTextDocument::modificationChanged,
293             this, &TextDocument::modificationChanged);
294     connect(&d->m_document, &QTextDocument::contentsChanged,
295             this, &Core::IDocument::contentsChanged);
296     connect(&d->m_document, &QTextDocument::contentsChange,
297             this, &TextDocument::contentsChangedWithPosition);
298 
299     // set new document layout
300     QTextOption opt = d->m_document.defaultTextOption();
301     opt.setTextDirection(Qt::LeftToRight);
302     opt.setFlags(opt.flags() | QTextOption::IncludeTrailingSpaces
303             | QTextOption::AddSpaceForLineAndParagraphSeparators
304             );
305     d->m_document.setDefaultTextOption(opt);
306     d->m_document.setDocumentLayout(new TextDocumentLayout(&d->m_document));
307 
308     if (id.isValid())
309         setId(id);
310 
311     setSuspendAllowed(true);
312 }
313 
~TextDocument()314 TextDocument::~TextDocument()
315 {
316     delete d;
317 }
318 
openedTextDocumentContents()319 QMap<QString, QString> TextDocument::openedTextDocumentContents()
320 {
321     QMap<QString, QString> workingCopy;
322     foreach (IDocument *document, DocumentModel::openedDocuments()) {
323         auto textEditorDocument = qobject_cast<TextDocument *>(document);
324         if (!textEditorDocument)
325             continue;
326         QString fileName = textEditorDocument->filePath().toString();
327         workingCopy[fileName] = textEditorDocument->plainText();
328     }
329     return workingCopy;
330 }
331 
openedTextDocumentEncodings()332 QMap<QString, QTextCodec *> TextDocument::openedTextDocumentEncodings()
333 {
334     QMap<QString, QTextCodec *> workingCopy;
335     foreach (IDocument *document, DocumentModel::openedDocuments()) {
336         auto textEditorDocument = qobject_cast<TextDocument *>(document);
337         if (!textEditorDocument)
338             continue;
339         QString fileName = textEditorDocument->filePath().toString();
340         workingCopy[fileName] = const_cast<QTextCodec *>(textEditorDocument->codec());
341     }
342     return workingCopy;
343 }
344 
currentTextDocument()345 TextDocument *TextDocument::currentTextDocument()
346 {
347     return qobject_cast<TextDocument *>(EditorManager::currentDocument());
348 }
349 
textDocumentForFilePath(const Utils::FilePath & filePath)350 TextDocument *TextDocument::textDocumentForFilePath(const Utils::FilePath &filePath)
351 {
352     return qobject_cast<TextDocument *>(DocumentModel::documentForFilePath(filePath));
353 }
354 
plainText() const355 QString TextDocument::plainText() const
356 {
357     return document()->toPlainText();
358 }
359 
textAt(int pos,int length) const360 QString TextDocument::textAt(int pos, int length) const
361 {
362     return Utils::Text::textAt(QTextCursor(document()), pos, length);
363 }
364 
characterAt(int pos) const365 QChar TextDocument::characterAt(int pos) const
366 {
367     return document()->characterAt(pos);
368 }
369 
setTypingSettings(const TypingSettings & typingSettings)370 void TextDocument::setTypingSettings(const TypingSettings &typingSettings)
371 {
372     d->m_typingSettings = typingSettings;
373 }
374 
setStorageSettings(const StorageSettings & storageSettings)375 void TextDocument::setStorageSettings(const StorageSettings &storageSettings)
376 {
377     d->m_storageSettings = storageSettings;
378 }
379 
typingSettings() const380 const TypingSettings &TextDocument::typingSettings() const
381 {
382     return d->m_typingSettings;
383 }
384 
storageSettings() const385 const StorageSettings &TextDocument::storageSettings() const
386 {
387     return d->m_storageSettings;
388 }
389 
setTabSettings(const TabSettings & newTabSettings)390 void TextDocument::setTabSettings(const TabSettings &newTabSettings)
391 {
392     if (newTabSettings == d->m_tabSettings)
393         return;
394     d->m_tabSettings = newTabSettings;
395 
396     emit tabSettingsChanged();
397 }
398 
tabSettings() const399 TabSettings TextDocument::tabSettings() const
400 {
401     return d->m_tabSettings;
402 }
403 
setFontSettings(const FontSettings & fontSettings)404 void TextDocument::setFontSettings(const FontSettings &fontSettings)
405 {
406     if (fontSettings == d->m_fontSettings)
407         return;
408     d->m_fontSettings = fontSettings;
409     d->m_fontSettingsNeedsApply = true;
410     emit fontSettingsChanged();
411 }
412 
createDiffAgainstCurrentFileAction(QObject * parent,const std::function<Utils::FilePath ()> & filePath)413 QAction *TextDocument::createDiffAgainstCurrentFileAction(
414     QObject *parent, const std::function<Utils::FilePath()> &filePath)
415 {
416     const auto diffAgainstCurrentFile = [filePath]() {
417         auto diffService = DiffService::instance();
418         auto textDocument = TextEditor::TextDocument::currentTextDocument();
419         const QString leftFilePath = textDocument ? textDocument->filePath().toString() : QString();
420         const QString rightFilePath = filePath().toString();
421         if (diffService && !leftFilePath.isEmpty() && !rightFilePath.isEmpty())
422             diffService->diffFiles(leftFilePath, rightFilePath);
423     };
424     auto diffAction = new QAction(tr("Diff Against Current File"), parent);
425     QObject::connect(diffAction, &QAction::triggered, parent, diffAgainstCurrentFile);
426     return diffAction;
427 }
428 
triggerPendingUpdates()429 void TextDocument::triggerPendingUpdates()
430 {
431     if (d->m_fontSettingsNeedsApply)
432         applyFontSettings();
433 }
434 
setCompletionAssistProvider(CompletionAssistProvider * provider)435 void TextDocument::setCompletionAssistProvider(CompletionAssistProvider *provider)
436 {
437     d->m_completionAssistProvider = provider;
438 }
439 
completionAssistProvider() const440 CompletionAssistProvider *TextDocument::completionAssistProvider() const
441 {
442     return d->m_completionAssistProvider;
443 }
444 
setFunctionHintAssistProvider(CompletionAssistProvider * provider)445 void TextDocument::setFunctionHintAssistProvider(CompletionAssistProvider *provider)
446 {
447     d->m_functionHintAssistProvider = provider;
448 }
449 
functionHintAssistProvider() const450 CompletionAssistProvider *TextDocument::functionHintAssistProvider() const
451 {
452     return d->m_functionHintAssistProvider;
453 }
454 
setQuickFixAssistProvider(IAssistProvider * provider) const455 void TextDocument::setQuickFixAssistProvider(IAssistProvider *provider) const
456 {
457     d->m_quickFixProvider = provider;
458 }
459 
quickFixAssistProvider() const460 IAssistProvider *TextDocument::quickFixAssistProvider() const
461 {
462     return d->m_quickFixProvider;
463 }
464 
applyFontSettings()465 void TextDocument::applyFontSettings()
466 {
467     d->m_fontSettingsNeedsApply = false;
468     if (d->m_highlighter) {
469         d->m_highlighter->setFontSettings(d->m_fontSettings);
470         d->m_highlighter->rehighlight();
471     }
472 }
473 
fontSettings() const474 const FontSettings &TextDocument::fontSettings() const
475 {
476     return d->m_fontSettings;
477 }
478 
setExtraEncodingSettings(const ExtraEncodingSettings & extraEncodingSettings)479 void TextDocument::setExtraEncodingSettings(const ExtraEncodingSettings &extraEncodingSettings)
480 {
481     d->m_extraEncodingSettings = extraEncodingSettings;
482 }
483 
autoIndent(const QTextCursor & cursor,QChar typedChar,int currentCursorPosition)484 void TextDocument::autoIndent(const QTextCursor &cursor, QChar typedChar, int currentCursorPosition)
485 {
486     d->m_indenter->indent(cursor, typedChar, tabSettings(), currentCursorPosition);
487 }
488 
autoReindent(const QTextCursor & cursor,int currentCursorPosition)489 void TextDocument::autoReindent(const QTextCursor &cursor, int currentCursorPosition)
490 {
491     d->m_indenter->reindent(cursor, tabSettings(), currentCursorPosition);
492 }
493 
autoFormatOrIndent(const QTextCursor & cursor)494 void TextDocument::autoFormatOrIndent(const QTextCursor &cursor)
495 {
496     d->m_indenter->autoIndent(cursor, tabSettings());
497 }
498 
indent(const QTextCursor & cursor,bool blockSelection,int column,int * offset)499 QTextCursor TextDocument::indent(const QTextCursor &cursor, bool blockSelection, int column,
500                                  int *offset)
501 {
502     return d->indentOrUnindent(cursor, true, tabSettings(), blockSelection, column, offset);
503 }
504 
unindent(const QTextCursor & cursor,bool blockSelection,int column,int * offset)505 QTextCursor TextDocument::unindent(const QTextCursor &cursor, bool blockSelection, int column,
506                                    int *offset)
507 {
508     return d->indentOrUnindent(cursor, false, tabSettings(), blockSelection, column, offset);
509 }
510 
setFormatter(Formatter * formatter)511 void TextDocument::setFormatter(Formatter *formatter)
512 {
513     d->m_formatter.reset(formatter);
514 }
515 
autoFormat(const QTextCursor & cursor)516 void TextDocument::autoFormat(const QTextCursor &cursor)
517 {
518     using namespace Utils::Text;
519     if (!d->m_formatter)
520         return;
521     if (QFutureWatcher<ChangeSet> *watcher = d->m_formatter->format(cursor, tabSettings())) {
522         connect(watcher, &QFutureWatcher<ChangeSet>::finished, this, [this, watcher]() {
523             if (!watcher->isCanceled())
524                 applyChangeSet(watcher->result());
525             delete watcher;
526         });
527     }
528 }
529 
applyChangeSet(const ChangeSet & changeSet)530 bool TextDocument::applyChangeSet(const ChangeSet &changeSet)
531 {
532     if (changeSet.isEmpty())
533         return true;
534     RefactoringChanges changes;
535     const RefactoringFilePtr file = changes.file(filePath());
536     file->setChangeSet(changeSet);
537     return file->apply();
538 }
539 
extraEncodingSettings() const540 const ExtraEncodingSettings &TextDocument::extraEncodingSettings() const
541 {
542     return d->m_extraEncodingSettings;
543 }
544 
setIndenter(Indenter * indenter)545 void TextDocument::setIndenter(Indenter *indenter)
546 {
547     // clear out existing code formatter data
548     for (QTextBlock it = document()->begin(); it.isValid(); it = it.next()) {
549         TextBlockUserData *userData = TextDocumentLayout::textUserData(it);
550         if (userData)
551             userData->setCodeFormatterData(nullptr);
552     }
553     d->m_indenter.reset(indenter);
554 }
555 
indenter() const556 Indenter *TextDocument::indenter() const
557 {
558     return d->m_indenter.data();
559 }
560 
isSaveAsAllowed() const561 bool TextDocument::isSaveAsAllowed() const
562 {
563     return true;
564 }
565 
fallbackSaveAsPath() const566 QString TextDocument::fallbackSaveAsPath() const
567 {
568     return d->m_defaultPath;
569 }
570 
fallbackSaveAsFileName() const571 QString TextDocument::fallbackSaveAsFileName() const
572 {
573     return d->m_suggestedFileName;
574 }
575 
setFallbackSaveAsPath(const QString & defaultPath)576 void TextDocument::setFallbackSaveAsPath(const QString &defaultPath)
577 {
578     d->m_defaultPath = defaultPath;
579 }
580 
setFallbackSaveAsFileName(const QString & suggestedFileName)581 void TextDocument::setFallbackSaveAsFileName(const QString &suggestedFileName)
582 {
583     d->m_suggestedFileName = suggestedFileName;
584 }
585 
document() const586 QTextDocument *TextDocument::document() const
587 {
588     return &d->m_document;
589 }
590 
syntaxHighlighter() const591 SyntaxHighlighter *TextDocument::syntaxHighlighter() const
592 {
593     return d->m_highlighter;
594 }
595 
596 /*!
597  * Saves the document to the file specified by \a fileName. If errors occur,
598  * \a errorString contains their cause.
599  * \a autoSave returns whether this function was called by the automatic save routine.
600  * If \a autoSave is true, the cursor will be restored and some signals suppressed
601  * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()).
602  */
save(QString * errorString,const FilePath & filePath,bool autoSave)603 bool TextDocument::save(QString *errorString, const FilePath &filePath, bool autoSave)
604 {
605     QTextCursor cursor(&d->m_document);
606 
607     // When autosaving, we don't want to modify the document/location under the user's fingers.
608     TextEditorWidget *editorWidget = nullptr;
609     int savedPosition = 0;
610     int savedAnchor = 0;
611     int savedVScrollBarValue = 0;
612     int savedHScrollBarValue = 0;
613     int undos = d->m_document.availableUndoSteps();
614 
615     // When saving the current editor, make sure to maintain the cursor and scroll bar
616     // positions for undo
617     if (BaseTextEditor *editor = BaseTextEditor::currentTextEditor()) {
618         if (editor->document() == this) {
619             editorWidget = editor->editorWidget();
620             QTextCursor cur = editor->textCursor();
621             savedPosition = cur.position();
622             savedAnchor = cur.anchor();
623             savedVScrollBarValue = editorWidget->verticalScrollBar()->value();
624             savedHScrollBarValue = editorWidget->horizontalScrollBar()->value();
625             cursor.setPosition(cur.position());
626         }
627     }
628 
629     if (!autoSave) {
630         cursor.beginEditBlock();
631         cursor.movePosition(QTextCursor::Start);
632 
633         if (d->m_storageSettings.m_cleanWhitespace) {
634             cleanWhitespace(cursor,
635                             d->m_storageSettings.m_inEntireDocument,
636                             d->m_storageSettings.m_cleanIndentation);
637         }
638         if (d->m_storageSettings.m_addFinalNewLine)
639           ensureFinalNewLine(cursor);
640         cursor.endEditBlock();
641     }
642 
643     const Utils::FilePath &savePath = filePath.isEmpty() ? this->filePath() : filePath;
644 
645     // check if UTF8-BOM has to be added or removed
646     Utils::TextFileFormat saveFormat = format();
647     if (saveFormat.codec->name() == "UTF-8" && supportsUtf8Bom()) {
648         switch (d->m_extraEncodingSettings.m_utf8BomSetting) {
649         case ExtraEncodingSettings::AlwaysAdd:
650             saveFormat.hasUtf8Bom = true;
651             break;
652         case ExtraEncodingSettings::OnlyKeep:
653             break;
654         case ExtraEncodingSettings::AlwaysDelete:
655             saveFormat.hasUtf8Bom = false;
656             break;
657         }
658     }
659 
660     const bool ok = write(savePath, saveFormat, d->m_document.toPlainText(), errorString);
661 
662     // restore text cursor and scroll bar positions
663     if (autoSave && undos < d->m_document.availableUndoSteps()) {
664         d->m_document.undo();
665         if (editorWidget) {
666             QTextCursor cur = editorWidget->textCursor();
667             cur.setPosition(savedAnchor);
668             cur.setPosition(savedPosition, QTextCursor::KeepAnchor);
669             editorWidget->verticalScrollBar()->setValue(savedVScrollBarValue);
670             editorWidget->horizontalScrollBar()->setValue(savedHScrollBarValue);
671             editorWidget->setTextCursor(cur);
672         }
673     }
674 
675     if (!ok)
676         return false;
677     d->m_autoSaveRevision = d->m_document.revision();
678     if (autoSave)
679         return true;
680 
681     // inform about the new filename
682     d->m_document.setModified(false); // also triggers update of the block revisions
683     setFilePath(savePath.absoluteFilePath());
684     emit changed();
685     return true;
686 }
687 
contents() const688 QByteArray TextDocument::contents() const
689 {
690     return plainText().toUtf8();
691 }
692 
setContents(const QByteArray & contents)693 bool TextDocument::setContents(const QByteArray &contents)
694 {
695     return setPlainText(QString::fromUtf8(contents));
696 }
697 
shouldAutoSave() const698 bool TextDocument::shouldAutoSave() const
699 {
700     return d->m_autoSaveRevision != d->m_document.revision();
701 }
702 
setFilePath(const Utils::FilePath & newName)703 void TextDocument::setFilePath(const Utils::FilePath &newName)
704 {
705     if (newName == filePath())
706         return;
707     IDocument::setFilePath(newName.absoluteFilePath());
708 }
709 
isModified() const710 bool TextDocument::isModified() const
711 {
712     return d->m_document.isModified();
713 }
714 
open(QString * errorString,const Utils::FilePath & filePath,const Utils::FilePath & realFilePath)715 Core::IDocument::OpenResult TextDocument::open(QString *errorString,
716                                                const Utils::FilePath &filePath,
717                                                const Utils::FilePath &realFilePath)
718 {
719     emit aboutToOpen(filePath, realFilePath);
720     OpenResult success = openImpl(errorString, filePath, realFilePath, /*reload =*/ false);
721     if (success == OpenResult::Success) {
722         setMimeType(Utils::mimeTypeForFile(filePath).name());
723         emit openFinishedSuccessfully();
724     }
725     return success;
726 }
727 
openImpl(QString * errorString,const Utils::FilePath & filePath,const Utils::FilePath & realFilePath,bool reload)728 Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString,
729                                                    const Utils::FilePath &filePath,
730                                                    const Utils::FilePath &realFilePath,
731                                                    bool reload)
732 {
733     QStringList content;
734 
735     ReadResult readResult = Utils::TextFileFormat::ReadIOError;
736 
737     if (!filePath.isEmpty()) {
738         readResult = read(realFilePath, &content, errorString);
739         const int chunks = content.size();
740 
741         // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different,
742         // since it will reset the undo's clear index
743         if (!reload || filePath == realFilePath)
744             d->m_document.setUndoRedoEnabled(reload);
745 
746         QTextCursor c(&d->m_document);
747         c.beginEditBlock();
748         if (reload) {
749             c.select(QTextCursor::Document);
750             c.removeSelectedText();
751         } else {
752             d->m_document.clear();
753         }
754 
755         if (chunks == 1) {
756             c.insertText(content.at(0));
757         } else if (chunks > 1) {
758             QFutureInterface<void> interface;
759             interface.setProgressRange(0, chunks);
760             ProgressManager::addTask(interface.future(), tr("Opening File"),
761                                      Constants::TASK_OPEN_FILE);
762             interface.reportStarted();
763 
764             for (int i = 0; i < chunks; ++i) {
765                 c.insertText(content.at(i));
766                 interface.setProgressValue(i + 1);
767                 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
768             }
769 
770             interface.reportFinished();
771         }
772 
773         c.endEditBlock();
774 
775         // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different,
776         // since it will reset the undo's clear index
777         if (!reload || filePath == realFilePath)
778             d->m_document.setUndoRedoEnabled(true);
779 
780         auto documentLayout =
781             qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
782         QTC_ASSERT(documentLayout, return OpenResult::CannotHandle);
783         documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document.revision();
784         d->updateRevisions();
785         d->m_document.setModified(filePath != realFilePath);
786         setFilePath(filePath);
787     }
788     if (readResult == Utils::TextFileFormat::ReadIOError)
789         return OpenResult::ReadError;
790     return OpenResult::Success;
791 }
792 
reload(QString * errorString,QTextCodec * codec)793 bool TextDocument::reload(QString *errorString, QTextCodec *codec)
794 {
795     QTC_ASSERT(codec, return false);
796     setCodec(codec);
797     return reload(errorString);
798 }
799 
reload(QString * errorString)800 bool TextDocument::reload(QString *errorString)
801 {
802     return reload(errorString, filePath());
803 }
804 
reload(QString * errorString,const FilePath & realFilePath)805 bool TextDocument::reload(QString *errorString, const FilePath &realFilePath)
806 {
807     emit aboutToReload();
808     auto documentLayout =
809         qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
810     TextMarks marks;
811     if (documentLayout)
812         marks = documentLayout->documentClosing(); // removes text marks non-permanently
813 
814     bool success = openImpl(errorString, filePath(), realFilePath, /*reload =*/true)
815                    == OpenResult::Success;
816 
817     if (documentLayout)
818         documentLayout->documentReloaded(marks, this); // re-adds text marks
819     emit reloadFinished(success);
820     return success;
821 }
822 
setPlainText(const QString & text)823 bool TextDocument::setPlainText(const QString &text)
824 {
825     if (text.size() > EditorManager::maxTextFileSize()) {
826         document()->setPlainText(TextEditorWidget::msgTextTooLarge(text.size()));
827         d->resetRevisions();
828         document()->setModified(false);
829         return false;
830     }
831     document()->setPlainText(text);
832     d->resetRevisions();
833     document()->setModified(false);
834     return true;
835 }
836 
reload(QString * errorString,ReloadFlag flag,ChangeType type)837 bool TextDocument::reload(QString *errorString, ReloadFlag flag, ChangeType type)
838 {
839     if (flag == FlagIgnore) {
840         if (type != TypeContents)
841             return true;
842 
843         const bool wasModified = document()->isModified();
844         {
845             Utils::GuardLocker locker(d->m_modificationChangedGuard);
846             // hack to ensure we clean the clear state in QTextDocument
847             document()->setModified(false);
848             document()->setModified(true);
849         }
850         if (!wasModified)
851             modificationChanged(true);
852         return true;
853     }
854     return reload(errorString);
855 }
856 
setSyntaxHighlighter(SyntaxHighlighter * highlighter)857 void TextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter)
858 {
859     if (d->m_highlighter)
860         delete d->m_highlighter;
861     d->m_highlighter = highlighter;
862     d->m_highlighter->setParent(this);
863     d->m_highlighter->setDocument(&d->m_document);
864 }
865 
cleanWhitespace(const QTextCursor & cursor)866 void TextDocument::cleanWhitespace(const QTextCursor &cursor)
867 {
868     bool hasSelection = cursor.hasSelection();
869     QTextCursor copyCursor = cursor;
870     copyCursor.setVisualNavigation(false);
871     copyCursor.beginEditBlock();
872 
873     cleanWhitespace(copyCursor, true, true);
874 
875     if (!hasSelection)
876         ensureFinalNewLine(copyCursor);
877 
878     copyCursor.endEditBlock();
879 }
880 
cleanWhitespace(QTextCursor & cursor,bool inEntireDocument,bool cleanIndentation)881 void TextDocument::cleanWhitespace(QTextCursor &cursor, bool inEntireDocument,
882                                    bool cleanIndentation)
883 {
884     const QString fileName(filePath().fileName());
885 
886     auto documentLayout = qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
887     Q_ASSERT(cursor.visualNavigation() == false);
888 
889     QTextBlock block = d->m_document.findBlock(cursor.selectionStart());
890     QTextBlock end;
891     if (cursor.hasSelection())
892         end = d->m_document.findBlock(cursor.selectionEnd()-1).next();
893 
894     QVector<QTextBlock> blocks;
895     while (block.isValid() && block != end) {
896         if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) {
897             blocks.append(block);
898         }
899         block = block.next();
900     }
901     if (blocks.isEmpty())
902         return;
903 
904     const TabSettings currentTabSettings = tabSettings();
905     const IndentationForBlock &indentations
906         = d->m_indenter->indentationForBlocks(blocks, currentTabSettings);
907 
908     foreach (block, blocks) {
909         QString blockText = block.text();
910 
911         if (d->m_storageSettings.removeTrailingWhitespace(fileName))
912             TabSettings::removeTrailingWhitespace(cursor, block);
913 
914         const int indent = indentations[block.blockNumber()];
915         if (cleanIndentation && !currentTabSettings.isIndentationClean(block, indent)) {
916             cursor.setPosition(block.position());
917             const int firstNonSpace = TabSettings::firstNonSpace(blockText);
918             if (firstNonSpace == blockText.length()) {
919                 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
920                 cursor.removeSelectedText();
921             } else {
922                 int column = currentTabSettings.columnAt(blockText, firstNonSpace);
923                 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
924                 QString indentationString = currentTabSettings.indentationString(0, column, column - indent, block);
925                 cursor.insertText(indentationString);
926             }
927         }
928     }
929 }
930 
ensureFinalNewLine(QTextCursor & cursor)931 void TextDocument::ensureFinalNewLine(QTextCursor& cursor)
932 {
933     if (!d->m_storageSettings.m_addFinalNewLine)
934         return;
935 
936     cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
937     bool emptyFile = !cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
938 
939     if (!emptyFile && cursor.selectedText().at(0) != QChar::ParagraphSeparator)
940     {
941         cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
942         cursor.insertText(QLatin1String("\n"));
943     }
944 }
945 
modificationChanged(bool modified)946 void TextDocument::modificationChanged(bool modified)
947 {
948     if (d->m_modificationChangedGuard.isLocked())
949         return;
950     // we only want to update the block revisions when going back to the saved version,
951     // e.g. with undo
952     if (!modified)
953         d->updateRevisions();
954     emit changed();
955 }
956 
updateLayout() const957 void TextDocument::updateLayout() const
958 {
959     auto documentLayout = qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
960     QTC_ASSERT(documentLayout, return);
961     documentLayout->requestUpdate();
962 }
963 
marks() const964 TextMarks TextDocument::marks() const
965 {
966     return d->m_marksCache;
967 }
968 
addMark(TextMark * mark)969 bool TextDocument::addMark(TextMark *mark)
970 {
971     if (mark->baseTextDocument())
972         return false;
973     QTC_ASSERT(mark->lineNumber() >= 1, return false);
974     int blockNumber = mark->lineNumber() - 1;
975     auto documentLayout = qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
976     QTC_ASSERT(documentLayout, return false);
977     QTextBlock block = d->m_document.findBlockByNumber(blockNumber);
978 
979     if (block.isValid()) {
980         TextBlockUserData *userData = TextDocumentLayout::userData(block);
981         userData->addMark(mark);
982         d->m_marksCache.append(mark);
983         mark->updateLineNumber(blockNumber + 1);
984         QTC_CHECK(mark->lineNumber() == blockNumber + 1); // Checks that the base class is called
985         mark->updateBlock(block);
986         mark->setBaseTextDocument(this);
987         if (!mark->isVisible())
988             return true;
989         // Update document layout
990         double newMaxWidthFactor = qMax(mark->widthFactor(), documentLayout->maxMarkWidthFactor);
991         bool fullUpdate =  newMaxWidthFactor > documentLayout->maxMarkWidthFactor || !documentLayout->hasMarks;
992         documentLayout->hasMarks = true;
993         documentLayout->maxMarkWidthFactor = newMaxWidthFactor;
994         if (fullUpdate)
995             documentLayout->requestUpdate();
996         else
997             documentLayout->requestExtraAreaUpdate();
998         return true;
999     }
1000     return false;
1001 }
1002 
marksAt(int line) const1003 TextMarks TextDocument::marksAt(int line) const
1004 {
1005     QTC_ASSERT(line >= 1, return TextMarks());
1006     int blockNumber = line - 1;
1007     QTextBlock block = d->m_document.findBlockByNumber(blockNumber);
1008 
1009     if (block.isValid()) {
1010         if (TextBlockUserData *userData = TextDocumentLayout::textUserData(block))
1011             return userData->marks();
1012     }
1013     return TextMarks();
1014 }
1015 
removeMarkFromMarksCache(TextMark * mark)1016 void TextDocument::removeMarkFromMarksCache(TextMark *mark)
1017 {
1018     auto documentLayout = qobject_cast<TextDocumentLayout*>(d->m_document.documentLayout());
1019     QTC_ASSERT(documentLayout, return);
1020     d->m_marksCache.removeAll(mark);
1021 
1022     auto scheduleLayoutUpdate = [documentLayout](){
1023         // make sure all destructors that may directly or indirectly call this function are
1024         // completed before updating.
1025         QMetaObject::invokeMethod(documentLayout, &QPlainTextDocumentLayout::requestUpdate,
1026                                   Qt::QueuedConnection);
1027     };
1028 
1029     if (d->m_marksCache.isEmpty()) {
1030         documentLayout->hasMarks = false;
1031         documentLayout->maxMarkWidthFactor = 1.0;
1032         scheduleLayoutUpdate();
1033         return;
1034     }
1035 
1036     if (!mark->isVisible())
1037         return;
1038 
1039     if (documentLayout->maxMarkWidthFactor == 1.0
1040             || mark->widthFactor() == 1.0
1041             || mark->widthFactor() < documentLayout->maxMarkWidthFactor) {
1042         // No change in width possible
1043         documentLayout->requestExtraAreaUpdate();
1044     } else {
1045         double maxWidthFactor = 1.0;
1046         foreach (const TextMark *mark, marks()) {
1047             if (!mark->isVisible())
1048                 continue;
1049             maxWidthFactor = qMax(mark->widthFactor(), maxWidthFactor);
1050             if (maxWidthFactor == documentLayout->maxMarkWidthFactor)
1051                 break; // Still a mark with the maxMarkWidthFactor
1052         }
1053 
1054         if (maxWidthFactor != documentLayout->maxMarkWidthFactor) {
1055             documentLayout->maxMarkWidthFactor = maxWidthFactor;
1056             scheduleLayoutUpdate();
1057         } else {
1058             documentLayout->requestExtraAreaUpdate();
1059         }
1060     }
1061 }
1062 
removeMark(TextMark * mark)1063 void TextDocument::removeMark(TextMark *mark)
1064 {
1065     QTextBlock block = d->m_document.findBlockByNumber(mark->lineNumber() - 1);
1066     if (auto data = static_cast<TextBlockUserData *>(block.userData())) {
1067         if (!data->removeMark(mark))
1068             qDebug() << "Could not find mark" << mark << "on line" << mark->lineNumber();
1069     }
1070 
1071     removeMarkFromMarksCache(mark);
1072     emit markRemoved(mark);
1073     mark->setBaseTextDocument(nullptr);
1074     updateLayout();
1075 }
1076 
updateMark(TextMark * mark)1077 void TextDocument::updateMark(TextMark *mark)
1078 {
1079     QTextBlock block = d->m_document.findBlockByNumber(mark->lineNumber() - 1);
1080     if (block.isValid()) {
1081         TextBlockUserData *userData = TextDocumentLayout::userData(block);
1082         // re-evaluate priority
1083         userData->removeMark(mark);
1084         userData->addMark(mark);
1085     }
1086     updateLayout();
1087 }
1088 
moveMark(TextMark * mark,int previousLine)1089 void TextDocument::moveMark(TextMark *mark, int previousLine)
1090 {
1091     QTextBlock block = d->m_document.findBlockByNumber(previousLine - 1);
1092     if (TextBlockUserData *data = TextDocumentLayout::textUserData(block)) {
1093         if (!data->removeMark(mark))
1094             qDebug() << "Could not find mark" << mark << "on line" << previousLine;
1095     }
1096     removeMarkFromMarksCache(mark);
1097     mark->setBaseTextDocument(nullptr);
1098     addMark(mark);
1099 }
1100 
1101 } // namespace TextEditor
1102