diff --git a/src/plugins/texteditor/textdocument.cpp b/src/plugins/texteditor/textdocument.cpp index 61c3b319b36..51fba77eb88 100644 --- a/src/plugins/texteditor/textdocument.cpp +++ b/src/plugins/texteditor/textdocument.cpp @@ -373,6 +373,13 @@ QAction *TextDocument::createDiffAgainstCurrentFileAction( return diffAction; } +void TextDocument::insertSuggestion(const QString text, const QTextBlock &block) +{ + TextDocumentLayout::userData(block)->setReplacement(block.text() + text); + TextDocumentLayout::updateReplacmentFormats(block, fontSettings()); + updateLayout(); +} + #ifdef WITH_TESTS void TextDocument::setSilentReload() { @@ -419,6 +426,12 @@ IAssistProvider *TextDocument::quickFixAssistProvider() const void TextDocument::applyFontSettings() { d->m_fontSettingsNeedsApply = false; + QTextBlock block = document()->firstBlock(); + while (block.isValid()) { + TextDocumentLayout::updateReplacmentFormats(block, fontSettings()); + block = block.next(); + } + updateLayout(); if (d->m_highlighter) { d->m_highlighter->setFontSettings(d->m_fontSettings); d->m_highlighter->rehighlight(); diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index dc52e1e9a8e..7a5797b7ca5 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -144,6 +144,8 @@ public: static QAction *createDiffAgainstCurrentFileAction(QObject *parent, const std::function &filePath); + void insertSuggestion(const QString text, const QTextBlock &block); + #ifdef WITH_TESTS void setSilentReload(); #endif diff --git a/src/plugins/texteditor/textdocumentlayout.cpp b/src/plugins/texteditor/textdocumentlayout.cpp index 1f26530dbb0..c160204e2f3 100644 --- a/src/plugins/texteditor/textdocumentlayout.cpp +++ b/src/plugins/texteditor/textdocumentlayout.cpp @@ -345,6 +345,13 @@ void TextBlockUserData::setCodeFormatterData(CodeFormatterData *data) m_codeFormatterData = data; } +void TextBlockUserData::setReplacement(const QString replacement) +{ + m_replacement.reset(new QTextDocument(replacement)); + m_replacement->setDocumentLayout(new TextDocumentLayout(m_replacement.get())); + m_replacement->setDocumentMargin(0); +} + void TextBlockUserData::addMark(TextMark *mark) { int i = 0; @@ -355,7 +362,6 @@ void TextBlockUserData::addMark(TextMark *mark) m_marks.insert(i, mark); } - TextDocumentLayout::TextDocumentLayout(QTextDocument *doc) : QPlainTextDocumentLayout(doc) {} @@ -519,6 +525,33 @@ QByteArray TextDocumentLayout::expectedRawStringSuffix(const QTextBlock &block) return {}; } +void TextDocumentLayout::updateReplacmentFormats(const QTextBlock &block, + const FontSettings &fontSettings) +{ + if (TextBlockUserData *userData = textUserData(block)) { + if (QTextDocument *replacement = userData->replacement()) { + const QTextCharFormat replacementFormat = fontSettings.toTextCharFormat( + TextStyles{C_TEXT, {C_DISABLED_CODE}}); + QTextCursor cursor(replacement); + cursor.select(QTextCursor::Document); + cursor.setCharFormat(fontSettings.toTextCharFormat(C_TEXT)); + cursor.setPosition(block.length() - 1); + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cursor.setCharFormat(replacementFormat); + replacement->firstBlock().layout()->setFormats(block.layout()->formats()); + } + } +} + +QString TextDocumentLayout::replacement(const QTextBlock &block) +{ + if (TextBlockUserData *userData = textUserData(block)) { + if (QTextDocument *replacement = userData->replacement()) + return replacement->toPlainText().mid(block.length() - 1); + } + return {}; +} + void TextDocumentLayout::requestExtraAreaUpdate() { emit updateExtraArea(); @@ -632,8 +665,28 @@ void TextDocumentLayout::requestUpdateNow() requestUpdate(); } +static QRectF replacementBoundingRect(const QTextDocument *replacement) +{ + QTC_ASSERT(replacement, return {}); + auto *layout = static_cast(replacement->documentLayout()); + QRectF boundingRect; + QTextBlock block = replacement->firstBlock(); + while (block.isValid()) { + const QRectF blockBoundingRect = layout->blockBoundingRect(block); + boundingRect.setWidth(std::max(boundingRect.width(), blockBoundingRect.width())); + boundingRect.setHeight(boundingRect.height() + blockBoundingRect.height()); + block = block.next(); + } + return boundingRect; +} + QRectF TextDocumentLayout::blockBoundingRect(const QTextBlock &block) const { + if (TextBlockUserData *userData = textUserData(block)) { + if (auto replacement = userData->replacement()) + return replacementBoundingRect(replacement); + } + QRectF boundingRect = QPlainTextDocumentLayout::blockBoundingRect(block); if (TextEditorSettings::fontSettings().relativeLineSpacing() != 100) { diff --git a/src/plugins/texteditor/textdocumentlayout.h b/src/plugins/texteditor/textdocumentlayout.h index ba31398db7b..5da2dd8f997 100644 --- a/src/plugins/texteditor/textdocumentlayout.h +++ b/src/plugins/texteditor/textdocumentlayout.h @@ -126,6 +126,10 @@ public: QByteArray expectedRawStringSuffix() { return m_expectedRawStringSuffix; } void setExpectedRawStringSuffix(const QByteArray &suffix) { m_expectedRawStringSuffix = suffix; } + void setReplacement(const QString replacement); + void clearReplacement() { m_replacement.reset(); } + QTextDocument *replacement() const { return m_replacement.get(); } + private: TextMarks m_marks; int m_foldingIndent : 16; @@ -139,6 +143,7 @@ private: CodeFormatterData *m_codeFormatterData; KSyntaxHighlighting::State m_syntaxState; QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic. + std::unique_ptr m_replacement; }; @@ -172,6 +177,8 @@ public: static void setFolded(const QTextBlock &block, bool folded); static void setExpectedRawStringSuffix(const QTextBlock &block, const QByteArray &suffix); static QByteArray expectedRawStringSuffix(const QTextBlock &block); + static void updateReplacmentFormats(const QTextBlock &block, const FontSettings &fontSettings); + static QString replacement(const QTextBlock &block); class TEXTEDITOR_EXPORT FoldValidator { diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index 9c4b54c20f3..ca0ba9268c9 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -819,6 +819,10 @@ public: QStack m_undoCursorStack; QList m_visualIndentCache; int m_visualIndentOffset = 0; + + void insertSuggestion(const QString &suggestion, const QTextBlock &block); + void clearCurrentSuggestion(); + QTextBlock m_suggestionBlock; }; class TextEditorWidgetFind : public BaseTextFind @@ -1646,6 +1650,26 @@ void TextEditorWidgetPrivate::handleMoveBlockSelection(QTextCursor::MoveOperatio q->setMultiTextCursor(MultiTextCursor(cursors)); } +void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion, const QTextBlock &block) +{ + clearCurrentSuggestion(); + m_suggestionBlock = block; + m_document->insertSuggestion(suggestion, block); + auto cursor = q->textCursor(); + cursor.setPosition(block.position()); + cursor.movePosition(QTextCursor::EndOfBlock); + q->setTextCursor(cursor); +} + +void TextEditorWidgetPrivate::clearCurrentSuggestion() +{ + if (TextBlockUserData *userData = TextDocumentLayout::textUserData(m_suggestionBlock)) { + userData->clearReplacement(); + m_document->updateLayout(); + } + m_suggestionBlock = QTextBlock(); +} + void TextEditorWidget::selectEncoding() { TextDocument *doc = d->m_document.data(); @@ -1828,6 +1852,17 @@ TextEditorWidget *TextEditorWidget::fromEditor(const IEditor *editor) void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded) { + if (m_suggestionBlock.isValid()) { + if (TextBlockUserData *data = TextDocumentLayout::textUserData(m_suggestionBlock)) { + if (auto replacementDocument = data->replacement()) { + if (replacementDocument->firstBlock().text().startsWith(m_suggestionBlock.text())) + TextDocumentLayout::updateReplacmentFormats(m_suggestionBlock, m_document->fontSettings()); + else + clearCurrentSuggestion(); + } + } + } + if (m_bracketsAnimator) m_bracketsAnimator->finish(); @@ -2506,6 +2541,11 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) d->m_maybeFakeTooltipEvent = false; if (e->key() == Qt::Key_Escape ) { TextEditorWidgetFind::cancelCurrentSelectAll(); + if (d->m_suggestionBlock.isValid()) { + d->clearCurrentSuggestion(); + e->accept(); + return; + } if (d->m_snippetOverlay->isVisible()) { e->accept(); d->m_snippetOverlay->accept(); @@ -2639,6 +2679,14 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) case Qt::Key_Tab: case Qt::Key_Backtab: { if (ro) break; + if (d->m_suggestionBlock.isValid()) { + QTextCursor cursor(d->m_suggestionBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + cursor.insertText(TextDocumentLayout::replacement(d->m_suggestionBlock)); + setTextCursor(cursor); + e->accept(); + return; + } if (d->m_snippetOverlay->isVisible() && !d->m_snippetOverlay->isEmpty()) { d->snippetTabOrBacktab(e->key() == Qt::Key_Tab); e->accept(); @@ -2684,6 +2732,8 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) | Qt::AltModifier | Qt::MetaModifier)) == Qt::NoModifier) { e->accept(); + if (d->m_suggestionBlock.isValid()) + d->clearCurrentSuggestion(); if (cursor.hasSelection()) { cursor.removeSelectedText(); setMultiTextCursor(cursor); @@ -2944,6 +2994,8 @@ void TextEditorWidgetPrivate::universalHelper() { // Test function for development. Place your new fangled experiment here to // give it proper scrutiny before pushing it onto others. + + insertSuggestion("Teste\nWeste\nBeste", q->textCursor().block()); } void TextEditorWidget::doSetTextCursor(const QTextCursor &cursor, bool keepMultiSelection) @@ -3105,7 +3157,9 @@ bool TextEditorWidget::event(QEvent *e) case QEvent::ShortcutOverride: { auto ke = static_cast(e); if (ke->key() == Qt::Key_Escape - && (d->m_snippetOverlay->isVisible() || multiTextCursor().hasMultipleCursors())) { + && (d->m_snippetOverlay->isVisible() + || multiTextCursor().hasMultipleCursors() + || d->m_suggestionBlock.isValid())) { e->accept(); } else { // hack copied from QInputControl::isCommonTextEditShortcut @@ -4394,7 +4448,22 @@ void TextEditorWidgetPrivate::paintAdditionalVisualWhitespaces(PaintEventData &d visualArrow); } if (!nextBlockIsValid) { // paint EOF symbol - QTextLine line = layout->lineAt(lineCount-1); + if (m_suggestionBlock.isValid() && data.block == m_suggestionBlock) { + if (TextBlockUserData *userData = TextDocumentLayout::textUserData( + m_suggestionBlock)) { + if (QTextDocument *replacement = userData->replacement()) { + const QTextBlock lastReplacementBlock = replacement->lastBlock(); + for (QTextBlock block = replacement->firstBlock(); + block != lastReplacementBlock && block.isValid(); + block = block.next()) { + top += replacement->documentLayout()->blockBoundingRect(block).height(); + } + layout = lastReplacementBlock.layout(); + lineCount = layout->lineCount(); + } + } + } + QTextLine line = layout->lineAt(lineCount - 1); QRectF lineRect = line.naturalTextRect().translated(data.offset.x(), top); int h = 4; lineRect.adjust(0, 0, -1, -1); @@ -4868,6 +4937,21 @@ void TextEditorWidget::paintBlock(QPainter *painter, const QVector &selections, const QRect &clipRect) const { + if (TextBlockUserData *userData = TextDocumentLayout::textUserData(block)) { + if (QTextDocument *replacement = userData->replacement()) { + QTextBlock replacementBlock = replacement->firstBlock(); + QPointF replacementOffset = offset; + replacementOffset.rx() += document()->documentMargin(); + while (replacementBlock.isValid()) { + replacementBlock.layout()->draw(painter, replacementOffset, selections, clipRect); + replacementOffset.ry() + += replacement->documentLayout()->blockBoundingRect(replacementBlock).height(); + replacementBlock = replacementBlock.next(); + } + return; + } + } + block.layout()->draw(painter, offset, selections, clipRect); } @@ -5405,6 +5489,12 @@ void TextEditorWidget::slotCursorPositionChanged() if (EditorManager::currentEditor() && EditorManager::currentEditor()->widget() == this) EditorManager::setLastEditLocation(EditorManager::currentEditor()); } + if (d->m_suggestionBlock.isValid()) { + if (textCursor().position() + != d->m_suggestionBlock.position() + d->m_suggestionBlock.length() - 1) { + d->clearCurrentSuggestion(); + } + } MultiTextCursor cursor = multiTextCursor(); cursor.replaceMainCursor(textCursor()); setMultiTextCursor(cursor); @@ -5849,6 +5939,16 @@ void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler) d->m_hoverHandlerRunner.handlerRemoved(handler); } +void TextEditorWidget::insertSuggestion(const QString &suggestion) +{ + d->insertSuggestion(suggestion, textCursor().block()); +} + +void TextEditorWidget::clearSuggestion() +{ + d->clearCurrentSuggestion(); +} + #ifdef WITH_TESTS void TextEditorWidget::processTooltipRequest(const QTextCursor &c) { diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index dc492eca21b..db20e0beb5f 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -469,6 +469,9 @@ public: void addHoverHandler(BaseHoverHandler *handler); void removeHoverHandler(BaseHoverHandler *handler); + void insertSuggestion(const QString &suggestion); + void clearSuggestion(); + #ifdef WITH_TESTS void processTooltipRequest(const QTextCursor &c); #endif