mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-01-30 20:00:13 -05:00
fear: Add hint-trigger for call code completion (#266)
This commit is contained in:
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
21
README.md
21
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
|
||||
|
||||
@ -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.";
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
72
widgets/CompletionHintHandler.cpp
Normal file
72
widgets/CompletionHintHandler.cpp
Normal 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
|
||||
|
||||
49
widgets/CompletionHintHandler.hpp
Normal file
49
widgets/CompletionHintHandler.hpp
Normal 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
|
||||
|
||||
83
widgets/CompletionHintWidget.cpp
Normal file
83
widgets/CompletionHintWidget.cpp
Normal 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
|
||||
|
||||
44
widgets/CompletionHintWidget.hpp
Normal file
44
widgets/CompletionHintWidget.hpp
Normal 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
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user