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