1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
16 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ** In addition, as a special exception, Nokia gives you certain additional
26 ** rights. These rights are described in the Nokia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
32 **************************************************************************/
34 #include "basetextdocument.h"
36 #include "basetextdocumentlayout.h"
37 #include "basetexteditor.h"
38 #include "storagesettings.h"
39 #include "tabsettings.h"
40 #include "syntaxhighlighter.h"
42 #include <QtCore/QFile>
43 #include <QtCore/QDir>
44 #include <QtCore/QFileInfo>
45 #include <QtCore/QTextStream>
46 #include <QtCore/QTextCodec>
47 #include <QtGui/QMainWindow>
48 #include <QtGui/QSyntaxHighlighter>
49 #include <QtGui/QApplication>
51 #include <coreplugin/editormanager/editormanager.h>
52 #include <coreplugin/icore.h>
53 #include <utils/qtcassert.h>
54 #include <utils/reloadpromptutils.h>
56 namespace TextEditor {
59 class DocumentMarker : public ITextMarkable
63 DocumentMarker(QTextDocument *);
66 bool addMark(ITextMark *mark, int line);
67 TextMarks marksAt(int line) const;
68 void removeMark(ITextMark *mark);
69 bool hasMark(ITextMark *mark) const;
70 void updateMark(ITextMark *mark);
73 QTextDocument *document;
76 DocumentMarker::DocumentMarker(QTextDocument *doc)
77 : ITextMarkable(doc), document(doc)
81 bool DocumentMarker::addMark(TextEditor::ITextMark *mark, int line)
83 QTC_ASSERT(line >= 1, return false);
84 int blockNumber = line - 1;
85 BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(document->documentLayout());
86 QTC_ASSERT(documentLayout, return false);
87 QTextBlock block = document->findBlockByNumber(blockNumber);
89 if (block.isValid()) {
90 TextBlockUserData *userData = BaseTextDocumentLayout::userData(block);
91 userData->addMark(mark);
92 mark->updateLineNumber(blockNumber + 1);
93 mark->updateBlock(block);
94 documentLayout->hasMarks = true;
95 documentLayout->requestUpdate();
101 TextEditor::TextMarks DocumentMarker::marksAt(int line) const
103 QTC_ASSERT(line >= 1, return TextMarks());
104 int blockNumber = line - 1;
105 QTextBlock block = document->findBlockByNumber(blockNumber);
107 if (block.isValid()) {
108 if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(block))
109 return userData->marks();
114 void DocumentMarker::removeMark(TextEditor::ITextMark *mark)
116 bool needUpdate = false;
117 QTextBlock block = document->begin();
118 while (block.isValid()) {
119 if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
120 needUpdate |= data->removeMark(mark);
122 block = block.next();
128 bool DocumentMarker::hasMark(TextEditor::ITextMark *mark) const
130 QTextBlock block = document->begin();
131 while (block.isValid()) {
132 if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
133 if (data->hasMark(mark))
136 block = block.next();
141 void DocumentMarker::updateMark(ITextMark *mark)
144 BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(document->documentLayout());
145 QTC_ASSERT(documentLayout, return);
146 documentLayout->requestUpdate();
149 } // namespace Internal
151 class BaseTextDocumentPrivate
154 explicit BaseTextDocumentPrivate(BaseTextDocument *q);
157 QString m_defaultPath;
158 QString m_suggestedFileName;
160 StorageSettings m_storageSettings;
161 TabSettings m_tabSettings;
162 QTextDocument *m_document;
163 Internal::DocumentMarker *m_documentMarker;
164 SyntaxHighlighter *m_highlighter;
166 enum LineTerminatorMode {
169 NativeLineTerminator =
170 #if defined (Q_OS_WIN)
176 LineTerminatorMode m_lineTerminatorMode;
178 bool m_fileHasUtf8Bom;
180 bool m_fileIsReadOnly;
182 bool m_hasDecodingError;
183 QByteArray m_decodingErrorSample;
186 BaseTextDocumentPrivate::BaseTextDocumentPrivate(BaseTextDocument *q) :
187 m_document(new QTextDocument(q)),
188 m_documentMarker(new Internal::DocumentMarker(m_document)),
190 m_lineTerminatorMode(NativeLineTerminator),
191 m_codec(Core::EditorManager::instance()->defaultTextEncoding()),
192 m_fileHasUtf8Bom(false),
193 m_fileIsReadOnly(false),
194 m_isBinaryData(false),
195 m_hasDecodingError(false)
199 BaseTextDocument::BaseTextDocument() : d(new BaseTextDocumentPrivate(this))
203 BaseTextDocument::~BaseTextDocument()
206 delete d->m_document;
211 QString BaseTextDocument::mimeType() const
213 return d->m_mimeType;
216 void BaseTextDocument::setMimeType(const QString &mt)
221 void BaseTextDocument::setStorageSettings(const StorageSettings &storageSettings)
223 d->m_storageSettings = storageSettings;
226 const StorageSettings &BaseTextDocument::storageSettings() const
228 return d->m_storageSettings;
231 void BaseTextDocument::setTabSettings(const TabSettings &tabSettings)
233 d->m_tabSettings = tabSettings;
236 const TabSettings &BaseTextDocument::tabSettings() const
238 return d->m_tabSettings;
241 QString BaseTextDocument::fileName() const
243 return d->m_fileName;
246 bool BaseTextDocument::isSaveAsAllowed() const
251 QString BaseTextDocument::defaultPath() const
253 return d->m_defaultPath;
256 QString BaseTextDocument::suggestedFileName() const
258 return d->m_suggestedFileName;
261 void BaseTextDocument::setDefaultPath(const QString &defaultPath)
263 d->m_defaultPath = defaultPath;
266 void BaseTextDocument::setSuggestedFileName(const QString &suggestedFileName)
268 d->m_suggestedFileName = suggestedFileName;
271 QTextDocument *BaseTextDocument::document() const
273 return d->m_document;
276 SyntaxHighlighter *BaseTextDocument::syntaxHighlighter() const
278 return d->m_highlighter;
281 bool BaseTextDocument::isBinaryData() const
283 return d->m_isBinaryData;
286 bool BaseTextDocument::hasDecodingError() const
288 return d->m_hasDecodingError || d->m_isBinaryData;
291 QTextCodec *BaseTextDocument::codec() const
296 void BaseTextDocument::setCodec(QTextCodec *c)
301 QByteArray BaseTextDocument::decodingErrorSample() const
303 return d->m_decodingErrorSample;
306 ITextMarkable *BaseTextDocument::documentMarker() const
308 return d->m_documentMarker;
311 bool BaseTextDocument::save(const QString &fileName)
313 QTextCursor cursor(d->m_document);
315 // When saving the current editor, make sure to maintain the cursor position for undo
316 Core::IEditor *currentEditor = Core::EditorManager::instance()->currentEditor();
317 if (BaseTextEditorEditable *editable = qobject_cast<BaseTextEditorEditable*>(currentEditor)) {
318 if (editable->file() == this)
319 cursor.setPosition(editable->editor()->textCursor().position());
322 cursor.beginEditBlock();
323 cursor.movePosition(QTextCursor::Start);
325 if (d->m_storageSettings.m_cleanWhitespace)
326 cleanWhitespace(cursor, d->m_storageSettings.m_cleanIndentation, d->m_storageSettings.m_inEntireDocument);
327 if (d->m_storageSettings.m_addFinalNewLine)
328 ensureFinalNewLine(cursor);
329 cursor.endEditBlock();
331 QString fName = d->m_fileName;
332 if (!fileName.isEmpty())
336 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
339 QString plainText = d->m_document->toPlainText();
341 if (d->m_lineTerminatorMode == BaseTextDocumentPrivate::CRLFLineTerminator)
342 plainText.replace(QLatin1Char('\n'), QLatin1String("\r\n"));
344 Core::IFile::Utf8BomSetting utf8bomSetting = Core::EditorManager::instance()->utf8BomSetting();
345 if (d->m_codec->name() == "UTF-8" &&
346 (utf8bomSetting == Core::IFile::AlwaysAdd || (utf8bomSetting == Core::IFile::OnlyKeep && d->m_fileHasUtf8Bom))) {
347 file.write("\xef\xbb\xbf", 3);
350 file.write(d->m_codec->fromUnicode(plainText));
355 const QFileInfo fi(fName);
356 d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
358 d->m_document->setModified(false);
359 emit titleChanged(fi.fileName());
362 d->m_isBinaryData = false;
363 d->m_hasDecodingError = false;
364 d->m_decodingErrorSample.clear();
369 void BaseTextDocument::rename(const QString &newName)
371 const QFileInfo fi(newName);
372 d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
373 emit titleChanged(fi.fileName());
377 bool BaseTextDocument::isReadOnly() const
379 if (d->m_isBinaryData || d->m_hasDecodingError)
381 if (d->m_fileName.isEmpty()) //have no corresponding file, so editing is ok
383 return d->m_fileIsReadOnly;
386 bool BaseTextDocument::isModified() const
388 return d->m_document->isModified();
391 void BaseTextDocument::checkPermissions()
393 bool previousReadOnly = d->m_fileIsReadOnly;
394 if (!d->m_fileName.isEmpty()) {
395 const QFileInfo fi(d->m_fileName);
396 d->m_fileIsReadOnly = !fi.isWritable();
398 d->m_fileIsReadOnly = false;
400 if (previousReadOnly != d->m_fileIsReadOnly)
404 bool BaseTextDocument::open(const QString &fileName)
406 QString title = tr("untitled");
407 if (!fileName.isEmpty()) {
408 const QFileInfo fi(fileName);
409 d->m_fileIsReadOnly = !fi.isWritable();
410 d->m_fileName = QDir::cleanPath(fi.absoluteFilePath());
412 QFile file(fileName);
413 if (!file.open(QIODevice::ReadOnly))
416 title = fi.fileName();
418 QByteArray buf = file.readAll();
419 int bytesRead = buf.size();
421 QTextCodec *codec = d->m_codec;
422 d->m_fileHasUtf8Bom = false;
424 // code taken from qtextstream
425 if (bytesRead >= 4 && ((uchar(buf[0]) == 0xff && uchar(buf[1]) == 0xfe && uchar(buf[2]) == 0 && uchar(buf[3]) == 0)
426 || (uchar(buf[0]) == 0 && uchar(buf[1]) == 0 && uchar(buf[2]) == 0xfe && uchar(buf[3]) == 0xff))) {
427 codec = QTextCodec::codecForName("UTF-32");
428 } else if (bytesRead >= 2 && ((uchar(buf[0]) == 0xff && uchar(buf[1]) == 0xfe)
429 || (uchar(buf[0]) == 0xfe && uchar(buf[1]) == 0xff))) {
430 codec = QTextCodec::codecForName("UTF-16");
431 } else if (bytesRead >= 3 && ((uchar(buf[0]) == 0xef && uchar(buf[1]) == 0xbb) && uchar(buf[2]) == 0xbf)) {
432 codec = QTextCodec::codecForName("UTF-8");
433 d->m_fileHasUtf8Bom = true;
435 codec = QTextCodec::codecForLocale();
437 // end code taken from qtextstream
441 #if 0 // should work, but does not, Qt bug with "system" codec
442 QTextDecoder *decoder = d->m_codec->makeDecoder();
443 QString text = decoder->toUnicode(buf);
444 d->m_hasDecodingError = (decoder->hasFailure());
447 QString text = d->m_codec->toUnicode(buf);
448 QByteArray verifyBuf = d->m_codec->fromUnicode(text); // slow
449 // the minSize trick lets us ignore unicode headers
450 int minSize = qMin(verifyBuf.size(), buf.size());
451 d->m_hasDecodingError = (minSize < buf.size()- 4
452 || memcmp(verifyBuf.constData() + verifyBuf.size() - minSize,
453 buf.constData() + buf.size() - minSize, minSize));
456 if (d->m_hasDecodingError) {
457 int p = buf.indexOf('\n', 16384);
459 d->m_decodingErrorSample = buf;
461 d->m_decodingErrorSample = buf.left(p);
463 d->m_decodingErrorSample.clear();
466 int lf = text.indexOf('\n');
467 if (lf > 0 && text.at(lf-1) == QLatin1Char('\r')) {
468 d->m_lineTerminatorMode = BaseTextDocumentPrivate::CRLFLineTerminator;
469 } else if (lf >= 0) {
470 d->m_lineTerminatorMode = BaseTextDocumentPrivate::LFLineTerminator;
472 d->m_lineTerminatorMode = BaseTextDocumentPrivate::NativeLineTerminator;
475 d->m_document->setModified(false);
476 if (d->m_isBinaryData)
477 d->m_document->setHtml(tr("<em>Binary data</em>"));
479 d->m_document->setPlainText(text);
480 BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
481 QTC_ASSERT(documentLayout, return true);
482 documentLayout->lastSaveRevision = d->m_document->revision();
483 d->m_document->setModified(false);
484 emit titleChanged(title);
490 void BaseTextDocument::reload(QTextCodec *codec)
492 QTC_ASSERT(codec, return);
497 void BaseTextDocument::reload()
499 emit aboutToReload();
500 documentClosing(); // removes text marks non-permanently
502 if (open(d->m_fileName))
506 Core::IFile::ReloadBehavior BaseTextDocument::reloadBehavior(ChangeTrigger state, ChangeType type) const
508 if (type == TypePermissions)
509 return BehaviorSilent;
510 if (type == TypeContents) {
511 if (state == TriggerInternal && !isModified())
512 return BehaviorSilent;
518 void BaseTextDocument::reload(ReloadFlag flag, ChangeType type)
520 if (flag == FlagIgnore)
522 if (type == TypePermissions) {
529 void BaseTextDocument::setSyntaxHighlighter(SyntaxHighlighter *highlighter)
531 if (d->m_highlighter)
532 delete d->m_highlighter;
533 d->m_highlighter = highlighter;
534 d->m_highlighter->setParent(this);
535 d->m_highlighter->setDocument(d->m_document);
540 void BaseTextDocument::cleanWhitespace(const QTextCursor &cursor)
542 bool hasSelection = cursor.hasSelection();
543 QTextCursor copyCursor = cursor;
544 copyCursor.setVisualNavigation(false);
545 copyCursor.beginEditBlock();
546 cleanWhitespace(copyCursor, true, true);
548 ensureFinalNewLine(copyCursor);
549 copyCursor.endEditBlock();
552 void BaseTextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument)
554 BaseTextDocumentLayout *documentLayout = qobject_cast<BaseTextDocumentLayout*>(d->m_document->documentLayout());
555 Q_ASSERT(cursor.visualNavigation() == false);
557 QTextBlock block = d->m_document->findBlock(cursor.selectionStart());
559 if (cursor.hasSelection())
560 end = d->m_document->findBlock(cursor.selectionEnd()-1).next();
562 while (block.isValid() && block != end) {
564 if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) {
566 QString blockText = block.text();
567 if (int trailing = d->m_tabSettings.trailingWhitespaces(blockText)) {
568 cursor.setPosition(block.position() + block.length() - 1);
569 cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, trailing);
570 cursor.removeSelectedText();
572 if (cleanIndentation && !d->m_tabSettings.isIndentationClean(block)) {
573 cursor.setPosition(block.position());
574 int firstNonSpace = d->m_tabSettings.firstNonSpace(blockText);
575 if (firstNonSpace == blockText.length()) {
576 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
577 cursor.removeSelectedText();
579 int column = d->m_tabSettings.columnAt(blockText, firstNonSpace);
580 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace);
581 QString indentationString = d->m_tabSettings.indentationString(0, column, block);
582 cursor.insertText(indentationString);
587 block = block.next();
591 void BaseTextDocument::ensureFinalNewLine(QTextCursor& cursor)
593 cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
594 bool emptyFile = !cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
596 if (!emptyFile && cursor.selectedText().at(0) != QChar::ParagraphSeparator)
598 cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
599 cursor.insertText(QLatin1String("\n"));
603 void BaseTextDocument::documentClosing()
605 QTextBlock block = d->m_document->begin();
606 while (block.isValid()) {
607 if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData()))
608 data->documentClosing();
609 block = block.next();
613 } // namespace TextEditor
615 #include "basetextdocument.moc"