diff --git a/src/libs/utils/stringutils.cpp b/src/libs/utils/stringutils.cpp index 7315a69da2a..37f9336d376 100644 --- a/src/libs/utils/stringutils.cpp +++ b/src/libs/utils/stringutils.cpp @@ -546,4 +546,26 @@ QTCREATOR_UTILS_EXPORT QPair splitAtFirst(const QStrin return splitAtFirst(view, ch); } +QTCREATOR_UTILS_EXPORT int endOfNextWord(const QString &string, int position) +{ + QTC_ASSERT(string.size() > position, return -1); + + static const QString wordSeparators = QStringLiteral(" \t\n\r()[]{}<>"); + + const auto predicate = [](const QChar &c) { return wordSeparators.contains(c); }; + + auto it = string.begin() + position; + if (predicate(*it)) + it = std::find_if_not(it, string.end(), predicate); + + if (it == string.end()) + return -1; + + it = std::find_if(it, string.end(), predicate); + if (it == string.end()) + return -1; + + return std::distance(string.begin(), it); +} + } // namespace Utils diff --git a/src/libs/utils/stringutils.h b/src/libs/utils/stringutils.h index 865a1dea122..d1a94330b7b 100644 --- a/src/libs/utils/stringutils.h +++ b/src/libs/utils/stringutils.h @@ -119,4 +119,6 @@ QTCREATOR_UTILS_EXPORT QPair splitAtFirst(const QStrin QTCREATOR_UTILS_EXPORT QPair splitAtFirst(const QStringView &stringView, QChar ch); +QTCREATOR_UTILS_EXPORT int endOfNextWord(const QString &string, int position = 0); + } // namespace Utils diff --git a/src/plugins/copilot/copilothoverhandler.cpp b/src/plugins/copilot/copilothoverhandler.cpp index 07aca2cbc7c..b252cedc77d 100644 --- a/src/plugins/copilot/copilothoverhandler.cpp +++ b/src/plugins/copilot/copilothoverhandler.cpp @@ -43,11 +43,14 @@ public: Tr::tr("Select Next Copilot Suggestion")); next->setEnabled(m_completions.size() > 1); - auto apply = addAction(Tr::tr("Apply (Tab)")); + auto apply = addAction(Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString())); + auto applyWord = addAction( + Tr::tr("Apply Word (%1)").arg(QKeySequence(QKeySequence::MoveToNextWord).toString())); connect(prev, &QAction::triggered, this, &CopilotCompletionToolTip::selectPrevious); connect(next, &QAction::triggered, this, &CopilotCompletionToolTip::selectNext); connect(apply, &QAction::triggered, this, &CopilotCompletionToolTip::apply); + connect(applyWord, &QAction::triggered, this, &CopilotCompletionToolTip::applyWord); updateLabels(); } @@ -88,8 +91,19 @@ private: void apply() { - if (TextSuggestion *suggestion = m_editor->currentSuggestion()) - suggestion->apply(); + if (TextSuggestion *suggestion = m_editor->currentSuggestion()) { + if (!suggestion->apply()) + return; + } + ToolTip::hide(); + } + + void applyWord() + { + if (TextSuggestion *suggestion = m_editor->currentSuggestion()) { + if (!suggestion->applyWord(m_editor)) + return; + } ToolTip::hide(); } diff --git a/src/plugins/copilot/copilotsuggestion.cpp b/src/plugins/copilot/copilotsuggestion.cpp index 96ccbbcd18a..de5660c02d1 100644 --- a/src/plugins/copilot/copilotsuggestion.cpp +++ b/src/plugins/copilot/copilotsuggestion.cpp @@ -3,6 +3,13 @@ #include "copilotsuggestion.h" +#include + +#include + +using namespace Utils; +using namespace TextEditor; + namespace Copilot::Internal { CopilotSuggestion::CopilotSuggestion(const QList &completions, @@ -27,6 +34,29 @@ bool CopilotSuggestion::apply() return true; } +bool CopilotSuggestion::applyWord(TextEditorWidget *widget) +{ + const Completion completion = m_completions.value(m_currentCompletion); + const QTextCursor cursor = completion.range().toSelection(m_start.document()); + QTextCursor currentCursor = widget->textCursor(); + const QString text = completion.text(); + const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock() + + (cursor.selectionEnd() - cursor.selectionStart()); + const int next = endOfNextWord(text, startPos); + + if (next == -1) + return apply(); + + // TODO: Allow adding more than one line + QString subText = text.mid(startPos, next - startPos); + subText = subText.left(subText.indexOf('\n')); + if (subText.isEmpty()) + return false; + + currentCursor.insertText(subText); + return false; +} + void CopilotSuggestion::reset() { m_start.removeSelectedText(); diff --git a/src/plugins/copilot/copilotsuggestion.h b/src/plugins/copilot/copilotsuggestion.h index a5b55b2d973..719016236a2 100644 --- a/src/plugins/copilot/copilotsuggestion.h +++ b/src/plugins/copilot/copilotsuggestion.h @@ -5,6 +5,7 @@ #include "requests/getcompletions.h" #include +#include namespace Copilot::Internal { @@ -16,6 +17,7 @@ public: int currentCompletion = 0); bool apply() final; + bool applyWord(TextEditor::TextEditorWidget *widget) final; void reset() final; int position() final; diff --git a/src/plugins/texteditor/textdocumentlayout.h b/src/plugins/texteditor/textdocumentlayout.h index 8ad013518c0..19278ad93c9 100644 --- a/src/plugins/texteditor/textdocumentlayout.h +++ b/src/plugins/texteditor/textdocumentlayout.h @@ -47,7 +47,10 @@ class TEXTEDITOR_EXPORT TextSuggestion public: TextSuggestion(); virtual ~TextSuggestion(); + // Returns true if the suggestion was applied completely, false if it was only partially applied. virtual bool apply() = 0; + // Returns true if the suggestion was applied completely, false if it was only partially applied. + virtual bool applyWord(TextEditorWidget *widget) = 0; virtual void reset() = 0; virtual int position() = 0; diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index d21644850c0..1ae22223a1a 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -2577,6 +2577,21 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) const bool inOverwriteMode = overwriteMode(); const bool hasMultipleCursors = cursor.hasMultipleCursors(); + if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(d->m_suggestionBlock)) { + if (e->matches(QKeySequence::MoveToNextWord)) { + e->accept(); + if (suggestion->applyWord(this)) + d->clearCurrentSuggestion(); + return; + } else if (e->modifiers() == Qt::NoModifier + && (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab)) { + e->accept(); + if (suggestion->apply()) + d->clearCurrentSuggestion(); + return; + } + } + if (!ro && (e == QKeySequence::InsertParagraphSeparator || (!d->m_lineSeparatorsAllowed && e == QKeySequence::InsertLineSeparator))) { @@ -2695,12 +2710,6 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) return; } QTextCursor cursor = textCursor(); - if (TextSuggestion *suggestion = TextDocumentLayout::suggestion(d->m_suggestionBlock)) { - suggestion->apply(); - d->clearCurrentSuggestion(); - e->accept(); - return; - } if (d->m_skipAutoCompletedText && e->key() == Qt::Key_Tab) { bool skippedAutoCompletedText = false; while (!d->m_autoCompleteHighlightPos.isEmpty()