From bcdec96d92a81da5d77b1b25225621ab017510fe Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:24:04 +0100 Subject: [PATCH] fear: Add hint-trigger for call code completion (#266) --- CMakeLists.txt | 2 + QodeAssistClient.cpp | 225 +++++++++++++++++++++----- QodeAssistClient.hpp | 9 ++ README.md | 21 +++ qodeassist.cpp | 6 +- settings/CodeCompletionSettings.cpp | 95 +++++++++-- settings/CodeCompletionSettings.hpp | 5 + settings/SettingsConstants.hpp | 5 + widgets/CompletionHintHandler.cpp | 72 +++++++++ widgets/CompletionHintHandler.hpp | 49 ++++++ widgets/CompletionHintWidget.cpp | 83 ++++++++++ widgets/CompletionHintWidget.hpp | 44 +++++ widgets/CompletionProgressHandler.cpp | 3 +- 13 files changed, 565 insertions(+), 54 deletions(-) create mode 100644 widgets/CompletionHintHandler.cpp create mode 100644 widgets/CompletionHintHandler.hpp create mode 100644 widgets/CompletionHintWidget.cpp create mode 100644 widgets/CompletionHintWidget.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ce8200c..27b57fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,8 @@ add_qtc_plugin(QodeAssist UpdateStatusWidget.hpp UpdateStatusWidget.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp + widgets/CompletionHintWidget.hpp widgets/CompletionHintWidget.cpp + widgets/CompletionHintHandler.hpp widgets/CompletionHintHandler.cpp widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 9d59db8..1866c3a 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -66,6 +66,10 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) m_typingTimer.start(); + m_hintHideTimer.setSingleShot(true); + m_hintHideTimer.setInterval(Settings::codeCompletionSettings().hintHideTimeout()); + connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); }); + m_refactorHoverHandler = new RefactorSuggestionHoverHandler(); } @@ -90,6 +94,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) widget->installEventFilter(this); } } + connect( document, &TextDocument::contentsChangedWithPosition, @@ -121,6 +126,12 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) if (charsRemoved > 0 || charsAdded <= 0) { m_recentCharCount = 0; m_typingTimer.restart(); + // 0 = Hint-based, 1 = Automatic + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + if (triggerMode != 1) { + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); + } return; } @@ -131,29 +142,35 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) if (lastChar.isEmpty() || lastChar[0].isPunct()) { m_recentCharCount = 0; m_typingTimer.restart(); + // 0 = Hint-based, 1 = Automatic + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + if (triggerMode != 1) { + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); + } return; } - m_recentCharCount += charsAdded; + bool isSpaceOrTab = lastChar[0].isSpace(); + + if (!isSpaceOrTab) { + m_recentCharCount += charsAdded; + } if (m_typingTimer.elapsed() > Settings::codeCompletionSettings().autoCompletionTypingInterval()) { - m_recentCharCount = charsAdded; + m_recentCharCount = isSpaceOrTab ? 0 : charsAdded; m_typingTimer.restart(); } - if (m_recentCharCount - > Settings::codeCompletionSettings().autoCompletionCharThreshold()) { - scheduleRequest(widget); + // 0 = Hint-based, 1 = Automatic + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + if (triggerMode == 1) { + handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab); + } else { + handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor); } }); - - // auto editors = BaseTextEditor::textEditorsForDocument(document); - // connect( - // editors.first()->editorWidget(), - // &TextEditorWidget::selectionChanged, - // this, - // [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); }); } bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project) @@ -168,6 +185,7 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor) if (!isEnabled(project)) return; + if (m_llmClient->contextManager() ->ignoreManager() ->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) { @@ -180,13 +198,18 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) return; + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + + if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) { + editor->abortAssist(); + } + const FilePath filePath = editor->textDocument()->filePath(); GetCompletionRequest request{ {TextDocumentIdentifier(hostPathToServerUri(filePath)), documentVersion(filePath), Position(cursor.mainCursor())}}; if (Settings::codeCompletionSettings().showProgressWidget()) { - // Setup cancel callback before showing progress m_progressHandler.setCancelCallback([this, editor = QPointer(editor)]() { if (editor) { cancelRunningRequest(editor); @@ -228,7 +251,6 @@ void QodeAssistClient::requestQuickRefactor( &QodeAssistClient::handleRefactoringResult); } - // Setup cancel callback before showing progress m_progressHandler.setCancelCallback([this, editor = QPointer(editor)]() { if (editor && m_refactorHandler) { m_refactorHandler->cancelRequest(); @@ -261,6 +283,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) }); connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] { cancelRunningRequest(editor); + // 0 = Hint-based, 1 = Automatic + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + if (triggerMode != 1) { + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); + } }); it = m_scheduledRequests.insert(editor, timer); } @@ -272,12 +300,16 @@ void QodeAssistClient::handleCompletions( const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) { m_progressHandler.hideProgress(); + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + + if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) { + editor->abortAssist(); + } if (response.error()) { log(*response.error()); - - QString errorMessage = tr("Code completion failed: %1").arg(response.error()->message()); - m_errorHandler.showError(editor, errorMessage); + m_errorHandler + .showError(editor, tr("Code completion failed: %1").arg(response.error()->message())); return; } @@ -299,14 +331,13 @@ void QodeAssistClient::handleCompletions( QList completions = Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion); - // remove trailing whitespaces from the end of the completions for (Completion &completion : completions) { const LanguageServerProtocol::Range range = completion.range(); if (range.start().line() != range.end().line()) - continue; // do not remove trailing whitespaces for multi-line replacements + continue; const QString completionText = completion.text(); - const int end = int(completionText.size()) - 1; // empty strings have been removed above + const int end = int(completionText.size()) - 1; int delta = 0; while (delta <= end && completionText[end - delta].isSpace()) ++delta; @@ -338,6 +369,12 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor if (it == m_runningRequests.constEnd()) return; m_progressHandler.hideProgress(); + // 0 = Hint-based, 1 = Automatic + const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); + if (triggerMode != 1) { + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); + } cancelRequest(it->id()); m_runningRequests.erase(it); } @@ -379,12 +416,22 @@ void QodeAssistClient::cleanupConnections() m_scheduledRequests.clear(); } +bool QodeAssistClient::isHintVisible() const +{ + return m_hintHandler.isHintVisible(); +} + +void QodeAssistClient::hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor) +{ + m_hintHandler.hideHint(); + requestCompletions(editor); +} + void QodeAssistClient::handleRefactoringResult(const RefactorResult &result) { m_progressHandler.hideProgress(); if (!result.success) { - // Show error to user QString errorMessage = result.errorMessage.isEmpty() ? tr("Quick refactor failed") : tr("Quick refactor failed: %1").arg(result.errorMessage); @@ -461,32 +508,128 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result) LOG_MESSAGE("Displaying refactoring suggestion with hover handler"); } +void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, + int charsAdded, + bool isSpaceOrTab) +{ + Q_UNUSED(isSpaceOrTab); + + if (m_recentCharCount + > Settings::codeCompletionSettings().autoCompletionCharThreshold()) { + scheduleRequest(widget); + } +} + +void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, + int charsAdded, + bool isSpaceOrTab, + QTextCursor &cursor) +{ + Q_UNUSED(charsAdded); + + const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold(); + if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) { + const QRect cursorRect = widget->cursorRect(cursor); + QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft()); + QPoint localPos = widget->mapFromGlobal(globalPos); + + int fontSize = widget->font().pixelSize(); + if (fontSize <= 0) { + fontSize = widget->fontMetrics().height(); + } + + QTextCursor textCursor = widget->textCursor(); + + if (m_recentCharCount <= hintThreshold) { + textCursor + .movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount); + } else { + textCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hintThreshold); + } + + int x = localPos.x() + cursorRect.height(); + int y = localPos.y() + cursorRect.height() / 4; + + QPoint hintPos(x, y); + + if (!m_hintHandler.isHintVisible()) { + m_hintHandler.showHint(widget, hintPos, fontSize); + } else { + m_hintHandler.updateHintPosition(widget, hintPos); + } + + m_hintHideTimer.start(); + } +} + bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event) { - if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { + auto *editor = qobject_cast(watched); + if (!editor) + return LanguageClient::Client::eventFilter(watched, event); + + if (event->type() == QEvent::KeyPress) { auto *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Escape) { - auto *editor = qobject_cast(watched); + // Check hint trigger key (0=Space, 1=Ctrl+Space, 2=Alt+Space, 3=Ctrl+Enter, 4=Tab, 5=Enter) + if (m_hintHandler.isHintVisible()) { + const int triggerKeyIndex = Settings::codeCompletionSettings().hintTriggerKey(); + bool isMatchingKey = false; + const Qt::KeyboardModifiers modifiers = keyEvent->modifiers(); - if (editor) { - if (m_runningRequests.contains(editor)) { - cancelRunningRequest(editor); - } - - if (m_scheduledRequests.contains(editor)) { - auto *timer = m_scheduledRequests.value(editor); - if (timer && timer->isActive()) { - timer->stop(); - } - } - - if (m_refactorHandler && m_refactorHandler->isProcessing()) { - m_refactorHandler->cancelRequest(); - } - - m_progressHandler.hideProgress(); + switch (triggerKeyIndex) { + case 0: // Space + isMatchingKey = (keyEvent->key() == Qt::Key_Space + && (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier)); + break; + case 1: // Ctrl+Space + isMatchingKey = (keyEvent->key() == Qt::Key_Space + && (modifiers & Qt::ControlModifier)); + break; + case 2: // Alt+Space + isMatchingKey = (keyEvent->key() == Qt::Key_Space + && (modifiers & Qt::AltModifier)); + break; + case 3: // Ctrl+Enter + isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) + && (modifiers & Qt::ControlModifier)); + break; + case 4: // Tab + isMatchingKey = (keyEvent->key() == Qt::Key_Tab); + break; + case 5: // Enter + isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) + && (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier)); + break; } + + if (isMatchingKey) { + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); + requestCompletions(editor); + return true; + } + } + + if (keyEvent->key() == Qt::Key_Escape) { + if (m_runningRequests.contains(editor)) { + cancelRunningRequest(editor); + } + + if (m_scheduledRequests.contains(editor)) { + auto *timer = m_scheduledRequests.value(editor); + if (timer && timer->isActive()) { + timer->stop(); + } + } + + if (m_refactorHandler && m_refactorHandler->isProcessing()) { + m_refactorHandler->cancelRequest(); + } + + m_progressHandler.hideProgress(); + m_hintHideTimer.stop(); + m_hintHandler.hideHint(); } } diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index b1d882a..2a127f5 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -32,6 +32,7 @@ #include "RefactorSuggestionHoverHandler.hpp" #include "widgets/CompletionProgressHandler.hpp" #include "widgets/CompletionErrorHandler.hpp" +#include "widgets/CompletionHintHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp" #include #include @@ -52,6 +53,9 @@ public: void requestCompletions(TextEditor::TextEditorWidget *editor); void requestQuickRefactor( TextEditor::TextEditorWidget *editor, const QString &instructions = QString()); + + bool isHintVisible() const; + void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor); protected: bool eventFilter(QObject *watched, QEvent *event) override; @@ -67,6 +71,9 @@ private: void cleanupConnections(); void handleRefactoringResult(const RefactorResult &result); + void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab); + void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor); + QHash m_runningRequests; QHash m_scheduledRequests; QMetaObject::Connection m_documentOpenedConnection; @@ -74,8 +81,10 @@ private: QElapsedTimer m_typingTimer; int m_recentCharCount; + QTimer m_hintHideTimer; CompletionProgressHandler m_progressHandler; CompletionErrorHandler m_errorHandler; + CompletionHintHandler m_hintHandler; EditorChatButtonHandler m_chatButtonHandler; QuickRefactorHandler *m_refactorHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; diff --git a/README.md b/README.md index ed49b00..045d3aa 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,27 @@ QodeAssist supports multiple LLM providers. Choose your preferred provider and f - Context-aware suggestions - Multiline completions +#### Completion Trigger Modes + +QodeAssist offers two trigger modes for code completion: + +**Hint-based (Default, Recommended)** +- Shows a hint indicator near cursor when you type 3+ characters +- Press **Space** (or custom key) to request completion +- **Best for**: Paid APIs (Claude, OpenAI), conscious control +- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption +- **Visual**: Clear indicator shows when completion is ready + +**Automatic** +- Automatically requests completion after typing threshold +- Works immediately without additional keypresses +- **Best for**: Local models (Ollama, llama.cpp), maximum automation +- **Benefits**: Hands-free experience, instant suggestions + +💡 **Tip**: Start with Hint-based to avoid unexpected costs. Switch to Automatic if using free local models. + +Configure in: `Tools → Options → QodeAssist → Code Completion → General Settings` + ### Chat Assistant - Multiple chat panels: side panel, bottom panel, and popup window - Chat history with auto-save and restore diff --git a/qodeassist.cpp b/qodeassist.cpp index 77d63f3..9e0d74a 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -146,7 +146,11 @@ public: requestAction.addOnTriggered(this, [this] { if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) { if (m_qodeAssistClient && m_qodeAssistClient->reachable()) { - m_qodeAssistClient->requestCompletions(editor); + if (m_qodeAssistClient->isHintVisible()) { + m_qodeAssistClient->hideHintAndRequestCompletion(editor); + } else { + m_qodeAssistClient->requestCompletions(editor); + } } else qWarning() << "The QodeAssist is not ready. Please check your connection and " "settings."; diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index c9b84dd..3d37c22 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -65,8 +65,21 @@ CodeCompletionSettings::CodeCompletionSettings() "as comments\n" "Raw Text: Shows unprocessed text without any formatting")); + completionTriggerMode.setLabelText(Tr::tr("Completion trigger mode:")); + completionTriggerMode.setSettingsKey(Constants::CC_COMPLETION_TRIGGER_MODE); + completionTriggerMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); + completionTriggerMode.addOption("Hint-based (Tab to trigger)"); + completionTriggerMode.addOption("Automatic"); + completionTriggerMode.setDefaultValue("Hint-based (Tab to trigger)"); + completionTriggerMode.setToolTip( + Tr::tr("Hint-based: Shows a hint when typing, press Tab to request completion\n" + "Automatic: Automatically requests completion after typing threshold")); + startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER); startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)")); + startSuggestionTimer.setToolTip( + Tr::tr("Delay before sending the completion request.\n" + "(Only for Automatic trigger mode)")); startSuggestionTimer.setRange(10, 10000); startSuggestionTimer.setDefaultValue(350); @@ -74,7 +87,8 @@ CodeCompletionSettings::CodeCompletionSettings() autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing")); autoCompletionCharThreshold.setToolTip( Tr::tr("The number of characters that need to be typed within the typing interval " - "before an AI suggestion request is sent.")); + "before an AI suggestion request is sent automatically.\n" + "(Only for Automatic trigger mode)")); autoCompletionCharThreshold.setRange(0, 10); autoCompletionCharThreshold.setDefaultValue(1); @@ -82,10 +96,42 @@ CodeCompletionSettings::CodeCompletionSettings() autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)")); autoCompletionTypingInterval.setToolTip( Tr::tr("The time window (in milliseconds) during which the character threshold " - "must be met to trigger an AI suggestion request.")); + "must be met to trigger an AI suggestion request automatically.\n" + "(Only for Automatic trigger mode)")); autoCompletionTypingInterval.setRange(500, 5000); autoCompletionTypingInterval.setDefaultValue(1200); + hintCharThreshold.setSettingsKey(Constants::CC_HINT_CHAR_THRESHOLD); + hintCharThreshold.setLabelText(Tr::tr("Hint shows after typing")); + hintCharThreshold.setToolTip( + Tr::tr("The number of characters that need to be typed before the hint widget appears " + "(only for Hint-based trigger mode).")); + hintCharThreshold.setRange(1, 10); + hintCharThreshold.setDefaultValue(3); + + hintHideTimeout.setSettingsKey(Constants::CC_HINT_HIDE_TIMEOUT); + hintHideTimeout.setLabelText(Tr::tr("Hint auto-hide timeout (ms)")); + hintHideTimeout.setToolTip( + Tr::tr("Time in milliseconds after which the hint widget will automatically hide " + "(only for Hint-based trigger mode).")); + hintHideTimeout.setRange(500, 10000); + hintHideTimeout.setDefaultValue(4000); + + hintTriggerKey.setLabelText(Tr::tr("Trigger key:")); + hintTriggerKey.setSettingsKey(Constants::CC_HINT_TRIGGER_KEY); + hintTriggerKey.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); + hintTriggerKey.addOption("Space"); + hintTriggerKey.addOption("Ctrl+Space"); + hintTriggerKey.addOption("Alt+Space"); + hintTriggerKey.addOption("Ctrl+Enter"); + hintTriggerKey.addOption("Tab"); + hintTriggerKey.addOption("Enter"); + hintTriggerKey.setDefaultValue("Space"); + hintTriggerKey.setToolTip( + Tr::tr("Key to press for requesting completion when hint is visible.\n" + "Space is recommended as least conflicting with context menu.\n" + "(Only for Hint-based trigger mode)")); + // General Parameters Settings temperature.setSettingsKey(Constants::CC_TEMPERATURE); temperature.setLabelText(Tr::tr("Temperature:")); @@ -212,6 +258,14 @@ CodeCompletionSettings::CodeCompletionSettings() showProgressWidget.setLabelText(Tr::tr("Show progress indicator during code completion")); showProgressWidget.setDefaultValue(true); + abortAssistOnRequest.setSettingsKey(Constants::CC_ABORT_ASSIST_ON_REQUEST); + abortAssistOnRequest.setLabelText(Tr::tr("Abort existing assist on new completion request")); + abortAssistOnRequest.setToolTip( + Tr::tr("When enabled, cancels any active Qt Creator code assist popup " + "before requesting LLM completion.\n" + "(Only for Automatic trigger mode)")); + abortAssistOnRequest.setDefaultValue(true); + useOpenFilesContext.setSettingsKey(Constants::CC_USE_OPEN_FILES_CONTEXT); useOpenFilesContext.setLabelText(Tr::tr("Include context from open files")); useOpenFilesContext.setDefaultValue(false); @@ -293,19 +347,33 @@ CodeCompletionSettings::CodeCompletionSettings() }}, Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}}; + auto generalSettings = Column{ + autoCompletion, + multiLineCompletion, + Row{modelOutputHandler, Stretch{1}}, + Row{completionTriggerMode, Stretch{1}}, + showProgressWidget, + useOpenFilesContext, + abortAssistOnRequest}; + + auto autoTriggerSettings = Column{ + Row{autoCompletionCharThreshold, + autoCompletionTypingInterval, + startSuggestionTimer, + Stretch{1}}}; + + auto hintTriggerSettings = Column{ + Row{hintCharThreshold, hintHideTimeout, Stretch{1}}, + Row{hintTriggerKey, Stretch{1}}}; + return Column{Row{Stretch{1}, resetToDefaults}, Space{8}, Group{title(TrConstants::AUTO_COMPLETION_SETTINGS), - Column{autoCompletion, + Column{Group{title(Tr::tr("General Settings")), generalSettings}, Space{8}, - multiLineCompletion, - Row{modelOutputHandler, Stretch{1}}, - Row{autoCompletionCharThreshold, - autoCompletionTypingInterval, - startSuggestionTimer, - Stretch{1}}, - showProgressWidget, - useOpenFilesContext}}, + Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}, + Space{8}, + Group{title(Tr::tr("Hint-based Trigger Mode")), hintTriggerSettings}}}, Space{8}, Group{title(Tr::tr("General Parameters")), Column{ @@ -389,6 +457,11 @@ void CodeCompletionSettings::resetSettingsToDefaults() resetAspect(useOpenFilesInQuickRefactor); resetAspect(quickRefactorSystemPrompt); resetAspect(modelOutputHandler); + resetAspect(completionTriggerMode); + resetAspect(hintCharThreshold); + resetAspect(hintHideTimeout); + resetAspect(hintTriggerKey); + resetAspect(abortAssistOnRequest); writeSettings(); } } diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp index ff56aad..caa9656 100644 --- a/settings/CodeCompletionSettings.hpp +++ b/settings/CodeCompletionSettings.hpp @@ -36,14 +36,19 @@ public: Utils::BoolAspect autoCompletion{this}; Utils::BoolAspect multiLineCompletion{this}; Utils::SelectionAspect modelOutputHandler{this}; + Utils::SelectionAspect completionTriggerMode{this}; Utils::IntegerAspect startSuggestionTimer{this}; Utils::IntegerAspect autoCompletionCharThreshold{this}; Utils::IntegerAspect autoCompletionTypingInterval{this}; + Utils::IntegerAspect hintCharThreshold{this}; + Utils::IntegerAspect hintHideTimeout{this}; + Utils::SelectionAspect hintTriggerKey{this}; Utils::StringListAspect customLanguages{this}; Utils::BoolAspect showProgressWidget{this}; + Utils::BoolAspect abortAssistOnRequest{this}; Utils::BoolAspect useOpenFilesContext{this}; // General Parameters Settings diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index e3d77cf..6521a9b 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -84,6 +84,11 @@ const char PROVIDER_PATHS[] = "QodeAssist.providerPaths"; const char СС_START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer"; const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold"; const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval"; +const char CC_COMPLETION_TRIGGER_MODE[] = "QodeAssist.ccCompletionTriggerMode"; +const char CC_HINT_CHAR_THRESHOLD[] = "QodeAssist.ccHintCharThreshold"; +const char CC_HINT_HIDE_TIMEOUT[] = "QodeAssist.ccHintHideTimeout"; +const char CC_HINT_TRIGGER_KEY[] = "QodeAssist.ccHintTriggerKey"; +const char CC_ABORT_ASSIST_ON_REQUEST[] = "QodeAssist.ccAbortAssistOnRequest"; const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold"; const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion"; const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler"; diff --git a/widgets/CompletionHintHandler.cpp b/widgets/CompletionHintHandler.cpp new file mode 100644 index 0000000..4a0a884 --- /dev/null +++ b/widgets/CompletionHintHandler.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "CompletionHintHandler.hpp" +#include "CompletionHintWidget.hpp" + +#include + +namespace QodeAssist { + +CompletionHintHandler::CompletionHintHandler() = default; + +CompletionHintHandler::~CompletionHintHandler() +{ + hideHint(); +} + +void CompletionHintHandler::showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize) +{ + if (!widget) { + return; + } + + if (!m_hintWidget) { + m_hintWidget = new CompletionHintWidget(widget, fontSize); + } + + m_hintWidget->move(position); + m_hintWidget->show(); + m_hintWidget->raise(); +} + +void CompletionHintHandler::hideHint() +{ + if (m_hintWidget) { + m_hintWidget->deleteLater(); + m_hintWidget = nullptr; + } +} + +bool CompletionHintHandler::isHintVisible() const +{ + return !m_hintWidget.isNull() && m_hintWidget->isVisible(); +} + +void CompletionHintHandler::updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position) +{ + if (!widget || !m_hintWidget) { + return; + } + + m_hintWidget->move(position); +} + +} // namespace QodeAssist + diff --git a/widgets/CompletionHintHandler.hpp b/widgets/CompletionHintHandler.hpp new file mode 100644 index 0000000..78e24d2 --- /dev/null +++ b/widgets/CompletionHintHandler.hpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include + +namespace TextEditor { +class TextEditorWidget; +} + +namespace QodeAssist { + +class CompletionHintWidget; + +class CompletionHintHandler +{ +public: + CompletionHintHandler(); + ~CompletionHintHandler(); + + void showHint(TextEditor::TextEditorWidget *widget, QPoint position, int fontSize); + void hideHint(); + bool isHintVisible() const; + void updateHintPosition(TextEditor::TextEditorWidget *widget, QPoint position); + +private: + QPointer m_hintWidget; +}; + +} // namespace QodeAssist + diff --git a/widgets/CompletionHintWidget.cpp b/widgets/CompletionHintWidget.cpp new file mode 100644 index 0000000..e9d6293 --- /dev/null +++ b/widgets/CompletionHintWidget.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "CompletionHintWidget.hpp" + +#include + +#include + +namespace QodeAssist { + +CompletionHintWidget::CompletionHintWidget(QWidget *parent, int fontSize) + : QWidget(parent) + , m_isHovered(false) +{ + m_accentColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal); + + setMouseTracking(true); + setFocusPolicy(Qt::NoFocus); + setAttribute(Qt::WA_TranslucentBackground); + + int triangleSize = qMax(6, fontSize / 2); + setFixedSize(triangleSize, triangleSize); +} + +void CompletionHintWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QColor triangleColor = m_accentColor; + triangleColor.setAlpha(m_isHovered ? 255 : 200); + + painter.setPen(Qt::NoPen); + painter.setBrush(triangleColor); + + QPolygonF triangle; + int w = width(); + int h = height(); + + triangle << QPointF(0, 0) + << QPointF(0, h) + << QPointF(w, h / 2.0); + + painter.drawPolygon(triangle); +} + +void CompletionHintWidget::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + m_isHovered = true; + setCursor(Qt::PointingHandCursor); + update(); +} + +void CompletionHintWidget::leaveEvent(QEvent *event) +{ + Q_UNUSED(event); + m_isHovered = false; + setCursor(Qt::ArrowCursor); + update(); +} + +} // namespace QodeAssist + diff --git a/widgets/CompletionHintWidget.hpp b/widgets/CompletionHintWidget.hpp new file mode 100644 index 0000000..ded4df1 --- /dev/null +++ b/widgets/CompletionHintWidget.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include + +namespace QodeAssist { + +class CompletionHintWidget : public QWidget +{ + Q_OBJECT +public: + explicit CompletionHintWidget(QWidget *parent = nullptr, int fontSize = 12); + ~CompletionHintWidget() override = default; + +protected: + void paintEvent(QPaintEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + QColor m_accentColor; + bool m_isHovered; +}; + +} // namespace QodeAssist + diff --git a/widgets/CompletionProgressHandler.cpp b/widgets/CompletionProgressHandler.cpp index 769ebf1..ded4aa2 100644 --- a/widgets/CompletionProgressHandler.cpp +++ b/widgets/CompletionProgressHandler.cpp @@ -88,7 +88,6 @@ void CompletionProgressHandler::operateTooltip( m_progressWidget = new ProgressWidget(editorWidget); - // Set cancel callback for the widget if (m_cancelCallback) { m_progressWidget->setCancelCallback(m_cancelCallback); } @@ -96,6 +95,8 @@ void CompletionProgressHandler::operateTooltip( const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); QPoint globalPos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft()); QPoint localPos = editorWidget->mapFromGlobal(globalPos); + + localPos.rx() += 5; localPos.ry() -= m_progressWidget->height() + 5; if (localPos.y() < 0) {