From 7cb585af87fec81b2cd23e7c47aea48440a63972 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Mon, 13 Mar 2023 15:11:50 +0100 Subject: [PATCH] TextEditor: support inline suggestions Change-Id: I70924a37f9078c5b33c1703e099fc9ddc0b1ae9a Reviewed-by: Marcus Tillmanns Reviewed-by: Qt CI Bot Reviewed-by: --- src/plugins/copilot/copilotclient.cpp | 17 +++- src/plugins/texteditor/textdocument.cpp | 14 +++- src/plugins/texteditor/textdocument.h | 2 +- src/plugins/texteditor/textdocumentlayout.cpp | 82 +++++++++++++++++-- src/plugins/texteditor/textdocumentlayout.h | 12 ++- src/plugins/texteditor/texteditor.cpp | 61 +++++++------- 6 files changed, 144 insertions(+), 44 deletions(-) diff --git a/src/plugins/copilot/copilotclient.cpp b/src/plugins/copilot/copilotclient.cpp index a6047004aa8..70eeb842bac 100644 --- a/src/plugins/copilot/copilotclient.cpp +++ b/src/plugins/copilot/copilotclient.cpp @@ -102,6 +102,7 @@ void CopilotClient::scheduleRequest(TextEditorWidget *editor) connect(timer, &QTimer::timeout, this, [this, editor]() { requestCompletions(editor); }); connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() { m_scheduledRequests.remove(editor); + cancelRunningRequest(editor); }); connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] { cancelRunningRequest(editor); @@ -129,8 +130,8 @@ void CopilotClient::requestCompletions(TextEditorWidget *editor) Position(cursor.mainCursor())}}; request.setResponseCallback([this, editor = QPointer(editor)]( const GetCompletionRequest::Response &response) { - if (editor) - handleCompletions(response, editor); + QTC_ASSERT(editor, return); + handleCompletions(response, editor); }); m_runningRequests[editor] = request; sendMessage(request); @@ -142,8 +143,16 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp if (response.error()) log(*response.error()); - Utils::MultiTextCursor cursor = editor->multiTextCursor(); - if (cursor.hasMultipleCursors() || cursor.hasSelection()) + int requestPosition = -1; + if (const auto requestParams = m_runningRequests.take(editor).params()) + requestPosition = requestParams->position().toPositionInDocument(editor->document()); + + const Utils::MultiTextCursor cursors = editor->multiTextCursor(); + if (cursors.hasMultipleCursors()) + return; + + const QTextCursor cursor = cursors.mainCursor(); + if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition) return; if (const std::optional result = response.result()) { diff --git a/src/plugins/texteditor/textdocument.cpp b/src/plugins/texteditor/textdocument.cpp index b6851411693..966edd4e7d8 100644 --- a/src/plugins/texteditor/textdocument.cpp +++ b/src/plugins/texteditor/textdocument.cpp @@ -373,10 +373,16 @@ QAction *TextDocument::createDiffAgainstCurrentFileAction( return diffAction; } -void TextDocument::insertSuggestion(const QString &text, const QTextBlock &block) +void TextDocument::insertSuggestion(const QString &text, const QTextCursor &cursor) { - TextDocumentLayout::userData(block)->setReplacement(block.text() + text); - TextDocumentLayout::updateReplacmentFormats(block, fontSettings()); + const QTextBlock block = cursor.block(); + const QString blockText = block.text(); + QString replacement = blockText.left(cursor.positionInBlock()) + text; + if (!text.contains('\n')) + replacement.append(blockText.mid(cursor.positionInBlock())); + TextDocumentLayout::userData(block)->setReplacement(replacement); + TextDocumentLayout::userData(block)->setReplacementPosition(cursor.positionInBlock()); + TextDocumentLayout::updateReplacementFormats(block, fontSettings()); updateLayout(); } @@ -428,7 +434,7 @@ void TextDocument::applyFontSettings() d->m_fontSettingsNeedsApply = false; QTextBlock block = document()->firstBlock(); while (block.isValid()) { - TextDocumentLayout::updateReplacmentFormats(block, fontSettings()); + TextDocumentLayout::updateReplacementFormats(block, fontSettings()); block = block.next(); } updateLayout(); diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index 8ddfe9effb1..9ef556503fd 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -144,7 +144,7 @@ public: static QAction *createDiffAgainstCurrentFileAction(QObject *parent, const std::function &filePath); - void insertSuggestion(const QString &text, const QTextBlock &block); + void insertSuggestion(const QString &text, const QTextCursor &cursor); #ifdef WITH_TESTS void setSilentReload(); diff --git a/src/plugins/texteditor/textdocumentlayout.cpp b/src/plugins/texteditor/textdocumentlayout.cpp index 8738f43bd15..ce337efa022 100644 --- a/src/plugins/texteditor/textdocumentlayout.cpp +++ b/src/plugins/texteditor/textdocumentlayout.cpp @@ -352,6 +352,17 @@ void TextBlockUserData::setReplacement(const QString &replacement) m_replacement->setDocumentMargin(0); } +void TextBlockUserData::setReplacementPosition(int replacementPosition) +{ + m_replacementPosition = replacementPosition; +} + +void TextBlockUserData::clearReplacement() +{ + m_replacement.reset(); + m_replacementPosition = -1; +} + void TextBlockUserData::addMark(TextMark *mark) { int i = 0; @@ -525,26 +536,64 @@ QByteArray TextDocumentLayout::expectedRawStringSuffix(const QTextBlock &block) return {}; } -void TextDocumentLayout::updateReplacmentFormats(const QTextBlock &block, +void TextDocumentLayout::updateReplacementFormats(const QTextBlock &block, const FontSettings &fontSettings) { if (QTextDocument *replacement = replacementDocument(block)) { const QTextCharFormat replacementFormat = fontSettings.toTextCharFormat( TextStyles{C_TEXT, {C_DISABLED_CODE}}); + QList formats = block.layout()->formats(); QTextCursor cursor(replacement); cursor.select(QTextCursor::Document); cursor.setCharFormat(fontSettings.toTextCharFormat(C_TEXT)); - cursor.setPosition(block.length() - 1); + const int position = replacementPosition(block); + cursor.setPosition(position); + const QString trailingText = block.text().mid(position); + if (!trailingText.isEmpty()) { + const int trailingIndex = replacement->firstBlock().text().indexOf(trailingText, + position); + if (trailingIndex >= 0) { + cursor.setPosition(trailingIndex, QTextCursor::KeepAnchor); + cursor.setCharFormat(replacementFormat); + cursor.setPosition(trailingIndex + trailingText.size()); + const int length = std::max(trailingIndex - position, 0); + if (length) { + // we have a replacement in the middle of the line adjust all formats that are + // behind the replacement + QTextLayout::FormatRange rest; + rest.start = -1; + for (QTextLayout::FormatRange &range : formats) { + if (range.start >= position) { + range.start += length; + } else if (range.start + range.length > position) { + // the format range starts before and ends after the position so we need to + // split the format into before and after the suggestion format ranges + rest.start = trailingIndex; + rest.length = range.length - (position - range.start); + rest.format = range.format; + range.length = position - range.start; + } + } + if (rest.start >= 0) + formats += rest; + } + } + } cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.setCharFormat(replacementFormat); - replacement->firstBlock().layout()->setFormats(block.layout()->formats()); + replacement->firstBlock().layout()->setFormats(formats); } } QString TextDocumentLayout::replacement(const QTextBlock &block) { - if (QTextDocument *replacement = replacementDocument(block)) - return replacement->toPlainText().mid(block.length() - 1); + if (QTextDocument *replacement = replacementDocument(block)) { + QTextCursor cursor(replacement); + const int position = replacementPosition(block); + cursor.setPosition(position); + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + return cursor.selectedText(); + } return {}; } @@ -554,6 +603,29 @@ QTextDocument *TextDocumentLayout::replacementDocument(const QTextBlock &block) return userData ? userData->replacement() : nullptr; } +int TextDocumentLayout::replacementPosition(const QTextBlock &block) +{ + TextBlockUserData *userData = textUserData(block); + return userData ? userData->replacementPosition() : -1; +} + +bool TextDocumentLayout::updateReplacement(const QTextBlock &block, + int position, + const FontSettings &fontSettings) +{ + if (QTextDocument *replacementDocument = TextDocumentLayout::replacementDocument(block)) { + const QString start = block.text().left(position); + const QString end = block.text().mid(position); + const QString replacement = replacementDocument->firstBlock().text(); + if (replacement.startsWith(start) && replacement.endsWith(end)) { + userData(block)->setReplacementPosition(position); + TextDocumentLayout::updateReplacementFormats(block, fontSettings); + return true; + } + } + return false; +} + void TextDocumentLayout::requestExtraAreaUpdate() { emit updateExtraArea(); diff --git a/src/plugins/texteditor/textdocumentlayout.h b/src/plugins/texteditor/textdocumentlayout.h index 7a2e7e948e9..97fa5992abb 100644 --- a/src/plugins/texteditor/textdocumentlayout.h +++ b/src/plugins/texteditor/textdocumentlayout.h @@ -127,8 +127,10 @@ public: void setExpectedRawStringSuffix(const QByteArray &suffix) { m_expectedRawStringSuffix = suffix; } void setReplacement(const QString &replacement); - void clearReplacement() { m_replacement.reset(); } + void setReplacementPosition(int replacementPosition); + void clearReplacement(); QTextDocument *replacement() const { return m_replacement.get(); } + int replacementPosition() const { return m_replacementPosition; } private: TextMarks m_marks; @@ -144,6 +146,7 @@ private: KSyntaxHighlighting::State m_syntaxState; QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic. std::unique_ptr m_replacement; + int m_replacementPosition = -1; }; @@ -177,9 +180,14 @@ 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 void updateReplacementFormats(const QTextBlock &block, + const FontSettings &fontSettings); static QString replacement(const QTextBlock &block); static QTextDocument *replacementDocument(const QTextBlock &block); + static int replacementPosition(const QTextBlock &block); + static bool updateReplacement(const QTextBlock &block, + int position, + const FontSettings &fontSettings); class TEXTEDITOR_EXPORT FoldValidator { diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index cc37fca79d5..b614a18e736 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -820,7 +820,8 @@ public: QList m_visualIndentCache; int m_visualIndentOffset = 0; - void insertSuggestion(const QString &suggestion, const QTextBlock &block); + void insertSuggestion(const QString &suggestion); + void updateSuggestion(); void clearCurrentSuggestion(); QTextBlock m_suggestionBlock; }; @@ -1650,15 +1651,28 @@ void TextEditorWidgetPrivate::handleMoveBlockSelection(QTextCursor::MoveOperatio q->setMultiTextCursor(MultiTextCursor(cursors)); } -void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion, const QTextBlock &block) +void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion) { clearCurrentSuggestion(); - m_suggestionBlock = block; - m_document->insertSuggestion(suggestion, block); auto cursor = q->textCursor(); - cursor.setPosition(block.position()); - cursor.movePosition(QTextCursor::EndOfBlock); - q->setTextCursor(cursor); + m_suggestionBlock = cursor.block(); + m_document->insertSuggestion(suggestion, cursor); +} + +void TextEditorWidgetPrivate::updateSuggestion() +{ + if (!m_suggestionBlock.isValid()) + return; + if (m_cursors.mainCursor().block() != m_suggestionBlock) { + clearCurrentSuggestion(); + } else { + const int position = m_cursors.mainCursor().position() - m_suggestionBlock.position(); + if (!TextDocumentLayout::updateReplacement(m_suggestionBlock, + position, + m_document->fontSettings())) { + clearCurrentSuggestion(); + } + } } void TextEditorWidgetPrivate::clearCurrentSuggestion() @@ -1852,16 +1866,7 @@ TextEditorWidget *TextEditorWidget::fromEditor(const IEditor *editor) void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded) { - if (m_suggestionBlock.isValid()) { - if (QTextDocument *replacementDocument = TextDocumentLayout::replacementDocument( - m_suggestionBlock)) { - if (replacementDocument->firstBlock().text().startsWith(m_suggestionBlock.text())) - TextDocumentLayout::updateReplacmentFormats(m_suggestionBlock, - m_document->fontSettings()); - else - clearCurrentSuggestion(); - } - } + updateSuggestion(); if (m_bracketsAnimator) m_bracketsAnimator->finish(); @@ -2680,10 +2685,15 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) 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); + const int position = TextDocumentLayout::replacementPosition(d->m_suggestionBlock); + if (position >= 0) { + QTextCursor cursor(d->m_suggestionBlock); + cursor.setPosition(d->m_suggestionBlock.position() + position); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + cursor.insertText(TextDocumentLayout::replacement(d->m_suggestionBlock)); + setTextCursor(cursor); + } + d->clearCurrentSuggestion(); e->accept(); return; } @@ -5481,17 +5491,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); d->updateCursorSelections(); d->updateHighlights(); + d->updateSuggestion(); } void TextEditorWidgetPrivate::updateHighlights() @@ -5933,7 +5938,7 @@ void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler) void TextEditorWidget::insertSuggestion(const QString &suggestion) { - d->insertSuggestion(suggestion, textCursor().block()); + d->insertSuggestion(suggestion); } void TextEditorWidget::clearSuggestion()