fear: Add hint-trigger for call code completion (#266)

This commit is contained in:
Petr Mironychev
2025-11-17 22:24:04 +01:00
committed by GitHub
parent 86c6930c5f
commit bcdec96d92
13 changed files with 565 additions and 54 deletions

View File

@ -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

View File

@ -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<TextEditorWidget>(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<TextEditorWidget>(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<Completion> 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<TextEditor::TextEditorWidget *>(watched);
if (!editor)
return LanguageClient::Client::eventFilter(watched, event);
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(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();
}
}

View File

@ -32,6 +32,7 @@
#include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/CompletionHintHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
@ -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<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> 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};

View File

@ -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

View File

@ -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.";

View File

@ -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();
}
}

View File

@ -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

View File

@ -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";

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "CompletionHintHandler.hpp"
#include "CompletionHintWidget.hpp"
#include <texteditor/texteditor.h>
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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QPointer>
#include <functional>
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<CompletionHintWidget> m_hintWidget;
};
} // namespace QodeAssist

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "CompletionHintWidget.hpp"
#include <QPainter>
#include <utils/theme/theme.h>
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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QWidget>
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

View File

@ -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) {