mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
refactor: Improvement code completion auto trigger
This commit is contained in:
@@ -54,6 +54,90 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
||||||
|
{
|
||||||
|
return Utils::Text::Position{pos.line, pos.column};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIdentifierChar(QChar c)
|
||||||
|
{
|
||||||
|
return c.isLetterOrNumber() || c == QLatin1Char('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInsideIdentifier(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0 || col > text.size())
|
||||||
|
return false;
|
||||||
|
if (!isIdentifierChar(text.at(col - 1)))
|
||||||
|
return false;
|
||||||
|
return col < text.size() && isIdentifierChar(text.at(col));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterMemberAccess(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && isIdentifierChar(text.at(i)))
|
||||||
|
--i;
|
||||||
|
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
if (c == QLatin1Char('.'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFreshIndentedLine(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
if (col == 0)
|
||||||
|
return false;
|
||||||
|
const QString leftText = block.text().left(col);
|
||||||
|
for (const QChar &ch : leftText) {
|
||||||
|
if (!ch.isSpace())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterEagerTrigger(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && text.at(i).isSpace())
|
||||||
|
--i;
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|
||||||
|
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|
||||||
|
|| c == QLatin1Char(':') || c == QLatin1Char('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isManualMode()
|
||||||
|
{
|
||||||
|
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
|
||||||
|
}
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||||
: LanguageClient::Client(clientInterface)
|
: LanguageClient::Client(clientInterface)
|
||||||
, m_llmClient(clientInterface)
|
, m_llmClient(clientInterface)
|
||||||
@@ -69,10 +153,6 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
|
|
||||||
m_typingTimer.start();
|
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();
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
@@ -108,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (isManualMode())
|
||||||
|
return;
|
||||||
|
|
||||||
auto project = ProjectManager::projectForFile(document->filePath());
|
auto project = ProjectManager::projectForFile(document->filePath());
|
||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
@@ -131,38 +214,29 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||||
m_recentCharCount = 0;
|
m_recentCharCount = 0;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor cursor = widget->textCursor();
|
QTextCursor cursor = widget->textCursor();
|
||||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||||
QString lastChar = cursor.selectedText();
|
const QString lastChar = cursor.selectedText();
|
||||||
|
if (lastChar.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
const QChar lastCh = lastChar[0];
|
||||||
|
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|
||||||
|
|| lastCh == QChar::LineSeparator) {
|
||||||
m_recentCharCount = 0;
|
m_recentCharCount = 0;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSpaceOrTab = lastChar[0].isSpace();
|
const bool isSpaceOrTab = lastCh.isSpace();
|
||||||
bool ignoreWhitespace
|
const bool ignoreWhitespace
|
||||||
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
||||||
|
|
||||||
if (!ignoreWhitespace || !isSpaceOrTab) {
|
if (!ignoreWhitespace || !isSpaceOrTab)
|
||||||
m_recentCharCount += charsAdded;
|
m_recentCharCount += charsAdded;
|
||||||
}
|
|
||||||
|
|
||||||
if (m_typingTimer.elapsed()
|
if (m_typingTimer.elapsed()
|
||||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||||
@@ -170,13 +244,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0 = Hint-based, 1 = Automatic
|
handleAutoRequestTrigger(widget);
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode == 1) {
|
|
||||||
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
|
|
||||||
} else {
|
|
||||||
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,11 +273,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
|
|
||||||
editor->abortAssist();
|
editor->abortAssist();
|
||||||
}
|
|
||||||
|
|
||||||
const FilePath filePath = editor->textDocument()->filePath();
|
const FilePath filePath = editor->textDocument()->filePath();
|
||||||
GetCompletionRequest request{
|
GetCompletionRequest request{
|
||||||
@@ -270,33 +336,25 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
|
|
||||||
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
cancelRunningRequest(editor);
|
if (m_runningRequests.contains(editor))
|
||||||
|
return;
|
||||||
|
|
||||||
auto it = m_scheduledRequests.find(editor);
|
auto it = m_scheduledRequests.find(editor);
|
||||||
if (it == m_scheduledRequests.end()) {
|
if (it == m_scheduledRequests.end()) {
|
||||||
auto timer = new QTimer(this);
|
auto timer = new QTimer(this);
|
||||||
timer->setSingleShot(true);
|
timer->setSingleShot(true);
|
||||||
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
||||||
if (editor
|
if (!editor || m_runningRequests.contains(editor))
|
||||||
&& editor->textCursor().position()
|
return;
|
||||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
if (editor->textCursor().position()
|
||||||
&& m_recentCharCount
|
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
return;
|
||||||
requestCompletions(editor);
|
requestCompletions(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||||
delete m_scheduledRequests.take(editor);
|
delete m_scheduledRequests.take(editor);
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(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);
|
it = m_scheduledRequests.insert(editor, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,11 +365,9 @@ void QodeAssistClient::handleCompletions(
|
|||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
|
|
||||||
editor->abortAssist();
|
editor->abortAssist();
|
||||||
}
|
|
||||||
|
|
||||||
if (response.error()) {
|
if (response.error()) {
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
@@ -325,12 +381,25 @@ void QodeAssistClient::handleCompletions(
|
|||||||
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
||||||
|
|
||||||
const MultiTextCursor cursors = editor->multiTextCursor();
|
const MultiTextCursor cursors = editor->multiTextCursor();
|
||||||
if (cursors.hasMultipleCursors())
|
if (cursors.hasMultipleCursors() || cursors.hasSelection())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
|
const int currentPosition = cursors.mainCursor().position();
|
||||||
|
if (requestPosition < 0 || currentPosition < requestPosition)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
QString typedSinceRequest;
|
||||||
|
if (currentPosition > requestPosition) {
|
||||||
|
QTextCursor diffCursor(editor->document());
|
||||||
|
diffCursor.setPosition(requestPosition);
|
||||||
|
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
|
||||||
|
typedSinceRequest = diffCursor.selectedText();
|
||||||
|
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|
||||||
|
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||||
auto isValidCompletion = [](const Completion &completion) {
|
auto isValidCompletion = [](const Completion &completion) {
|
||||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||||
@@ -338,34 +407,58 @@ void QodeAssistClient::handleCompletions(
|
|||||||
QList<Completion> completions
|
QList<Completion> completions
|
||||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||||
|
|
||||||
|
QList<Completion> matchedCompletions;
|
||||||
|
matchedCompletions.reserve(completions.size());
|
||||||
for (Completion &completion : completions) {
|
for (Completion &completion : completions) {
|
||||||
const LanguageServerProtocol::Range range = completion.range();
|
const LanguageServerProtocol::Range range = completion.range();
|
||||||
if (range.start().line() != range.end().line())
|
if (range.start().line() != range.end().line())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const QString completionText = completion.text();
|
QString completionText = completion.text();
|
||||||
const int end = int(completionText.size()) - 1;
|
const int end = int(completionText.size()) - 1;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
while (delta <= end && completionText[end - delta].isSpace())
|
while (delta <= end && completionText[end - delta].isSpace())
|
||||||
++delta;
|
++delta;
|
||||||
|
|
||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
completion.setText(completionText.chopped(delta));
|
completionText.chop(delta);
|
||||||
|
|
||||||
|
if (!typedSinceRequest.isEmpty()) {
|
||||||
|
if (!completionText.startsWith(typedSinceRequest))
|
||||||
|
continue;
|
||||||
|
completionText = completionText.mid(typedSinceRequest.size());
|
||||||
|
if (completionText.isEmpty())
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
|
||||||
|
completion.setText(completionText);
|
||||||
|
matchedCompletions.append(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedCompletions.isEmpty()) {
|
||||||
|
LOG_MESSAGE("No valid completions received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Text::Position anchor = typedSinceRequest.isEmpty()
|
||||||
|
? Text::Position{}
|
||||||
|
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
|
||||||
|
const bool useAnchor = !typedSinceRequest.isEmpty();
|
||||||
|
|
||||||
|
auto suggestions = Utils::transform(matchedCompletions,
|
||||||
|
[useAnchor, &anchor](const Completion &c) {
|
||||||
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||||
return Text::Position{pos.line() + 1, pos.character()};
|
return Text::Position{pos.line() + 1, pos.character()};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (useAnchor) {
|
||||||
|
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
|
||||||
|
}
|
||||||
|
|
||||||
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||||
Text::Position pos{toTextPos(c.position())};
|
Text::Position pos{toTextPos(c.position())};
|
||||||
return TextSuggestion::Data{range, pos, c.text()};
|
return TextSuggestion::Data{range, pos, c.text()};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (completions.isEmpty()) {
|
|
||||||
LOG_MESSAGE("No valid completions received");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,12 +469,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
|||||||
if (it == m_runningRequests.constEnd())
|
if (it == m_runningRequests.constEnd())
|
||||||
return;
|
return;
|
||||||
m_progressHandler.hideProgress();
|
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());
|
cancelRequest(it->id());
|
||||||
m_runningRequests.erase(it);
|
m_runningRequests.erase(it);
|
||||||
}
|
}
|
||||||
@@ -423,17 +510,6 @@ void QodeAssistClient::cleanupConnections()
|
|||||||
m_scheduledRequests.clear();
|
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)
|
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
@@ -465,13 +541,6 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
|
||||||
{
|
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
|
||||||
}
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
||||||
{
|
{
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
@@ -604,58 +673,20 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
|
|||||||
editCursor.endEditBlock();
|
editCursor.endEditBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
|
||||||
int charsAdded,
|
|
||||||
bool isSpaceOrTab)
|
|
||||||
{
|
{
|
||||||
Q_UNUSED(isSpaceOrTab);
|
const QTextCursor cursor = widget->textCursor();
|
||||||
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
const bool smart = settings.smartContextTrigger();
|
||||||
|
|
||||||
if (m_recentCharCount
|
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
return;
|
||||||
|
|
||||||
|
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
|
||||||
|
const int charThreshold = settings.autoCompletionCharThreshold();
|
||||||
|
|
||||||
|
if (eager || m_recentCharCount > charThreshold)
|
||||||
scheduleRequest(widget);
|
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)
|
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||||
@@ -667,46 +698,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
|||||||
if (event->type() == QEvent::KeyPress) {
|
if (event->type() == QEvent::KeyPress) {
|
||||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
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 (keyEvent->key() == Qt::Key_Escape) {
|
||||||
if (m_runningRequests.contains(editor)) {
|
if (m_runningRequests.contains(editor)) {
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
@@ -724,8 +715,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "widgets/CompletionProgressHandler.hpp"
|
#include "widgets/CompletionProgressHandler.hpp"
|
||||||
#include "widgets/CompletionErrorHandler.hpp"
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
#include "widgets/CompletionHintHandler.hpp"
|
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
#include "widgets/RefactorWidgetHandler.hpp"
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
@@ -35,9 +34,6 @@ public:
|
|||||||
void requestQuickRefactor(
|
void requestQuickRefactor(
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||||
|
|
||||||
bool isHintVisible() const;
|
|
||||||
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
@@ -55,8 +51,7 @@ private:
|
|||||||
void displayRefactoringWidget(const RefactorResult &result);
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||||
|
|
||||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
|
||||||
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
|
||||||
|
|
||||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||||
@@ -65,10 +60,8 @@ private:
|
|||||||
|
|
||||||
QElapsedTimer m_typingTimer;
|
QElapsedTimer m_typingTimer;
|
||||||
int m_recentCharCount;
|
int m_recentCharCount;
|
||||||
QTimer m_hintHideTimer;
|
|
||||||
CompletionProgressHandler m_progressHandler;
|
CompletionProgressHandler m_progressHandler;
|
||||||
CompletionErrorHandler m_errorHandler;
|
CompletionErrorHandler m_errorHandler;
|
||||||
CompletionHintHandler m_hintHandler;
|
|
||||||
EditorChatButtonHandler m_chatButtonHandler;
|
EditorChatButtonHandler m_chatButtonHandler;
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
|
|||||||
@@ -136,11 +136,7 @@ public:
|
|||||||
requestAction.addOnTriggered(this, [this] {
|
requestAction.addOnTriggered(this, [this] {
|
||||||
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
|
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
|
||||||
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
|
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
|
||||||
if (m_qodeAssistClient->isHintVisible()) {
|
|
||||||
m_qodeAssistClient->hideHintAndRequestCompletion(editor);
|
|
||||||
} else {
|
|
||||||
m_qodeAssistClient->requestCompletions(editor);
|
m_qodeAssistClient->requestCompletions(editor);
|
||||||
}
|
|
||||||
} else
|
} else
|
||||||
qWarning() << "The QodeAssist is not ready. Please check your connection and "
|
qWarning() << "The QodeAssist is not ready. Please check your connection and "
|
||||||
"settings.";
|
"settings.";
|
||||||
|
|||||||
@@ -59,6 +59,34 @@ CodeCompletionSettings::CodeCompletionSettings()
|
|||||||
Tr::tr("Hint-based: Shows a hint when typing, press Tab to request completion\n"
|
Tr::tr("Hint-based: Shows a hint when typing, press Tab to request completion\n"
|
||||||
"Automatic: Automatically requests completion after typing threshold"));
|
"Automatic: Automatically requests completion after typing threshold"));
|
||||||
|
|
||||||
|
completionMode.setLabelText(Tr::tr("Completion mode:"));
|
||||||
|
completionMode.setSettingsKey(Constants::CC_COMPLETION_MODE);
|
||||||
|
completionMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||||
|
completionMode.addOption("Automatic");
|
||||||
|
completionMode.addOption("Manual");
|
||||||
|
completionMode.setDefaultValue("Automatic");
|
||||||
|
completionMode.setToolTip(
|
||||||
|
Tr::tr("Automatic: requests completion while typing (with smart context gates).\n"
|
||||||
|
"Manual: no auto-triggering; invoke via the 'Request QodeAssist Suggestion' "
|
||||||
|
"shortcut (default Ctrl+Alt+Q, reconfigurable in Preferences > Keyboard)."));
|
||||||
|
|
||||||
|
smartContextTrigger.setSettingsKey(Constants::CC_SMART_CONTEXT_TRIGGER);
|
||||||
|
smartContextTrigger.setLabelText(Tr::tr("Smart context-aware triggering"));
|
||||||
|
smartContextTrigger.setDefaultValue(true);
|
||||||
|
smartContextTrigger.setToolTip(
|
||||||
|
Tr::tr("When enabled, auto-completion is suppressed in places where Qt Creator's built-in "
|
||||||
|
"completion is usually stronger (middle of an identifier, right after '.', '->', "
|
||||||
|
"'::') and is triggered more eagerly after structural characters like '(', ',', "
|
||||||
|
"'{', '=' and on fresh indented lines."));
|
||||||
|
|
||||||
|
respectQtcPopup.setSettingsKey(Constants::CC_RESPECT_QTC_POPUP);
|
||||||
|
respectQtcPopup.setLabelText(Tr::tr("Don't dismiss Qt Creator's completion popup"));
|
||||||
|
respectQtcPopup.setDefaultValue(true);
|
||||||
|
respectQtcPopup.setToolTip(
|
||||||
|
Tr::tr("When enabled, an AI completion arriving while Qt Creator's own completion popup "
|
||||||
|
"is already visible will not force it closed. The LLM suggestion still appears "
|
||||||
|
"inline."));
|
||||||
|
|
||||||
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
|
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
|
||||||
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
|
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
|
||||||
startSuggestionTimer.setToolTip(
|
startSuggestionTimer.setToolTip(
|
||||||
@@ -308,6 +336,8 @@ CodeCompletionSettings::CodeCompletionSettings()
|
|||||||
|
|
||||||
readSettings();
|
readSettings();
|
||||||
|
|
||||||
|
migrateCompletionMode();
|
||||||
|
|
||||||
readFileParts.setValue(!readFullFile.value());
|
readFileParts.setValue(!readFullFile.value());
|
||||||
|
|
||||||
setupConnections();
|
setupConnections();
|
||||||
@@ -354,30 +384,26 @@ CodeCompletionSettings::CodeCompletionSettings()
|
|||||||
autoCompletion,
|
autoCompletion,
|
||||||
multiLineCompletion,
|
multiLineCompletion,
|
||||||
Row{modelOutputHandler, Stretch{1}},
|
Row{modelOutputHandler, Stretch{1}},
|
||||||
Row{completionTriggerMode, Stretch{1}},
|
Row{completionMode, Stretch{1}},
|
||||||
showProgressWidget,
|
showProgressWidget,
|
||||||
useOpenFilesContext,
|
useOpenFilesContext,
|
||||||
|
respectQtcPopup,
|
||||||
abortAssistOnRequest,
|
abortAssistOnRequest,
|
||||||
ignoreWhitespaceInCharCount};
|
ignoreWhitespaceInCharCount};
|
||||||
|
|
||||||
auto autoTriggerSettings = Column{
|
auto autoTriggerSettings = Column{
|
||||||
|
smartContextTrigger,
|
||||||
Row{autoCompletionCharThreshold,
|
Row{autoCompletionCharThreshold,
|
||||||
autoCompletionTypingInterval,
|
autoCompletionTypingInterval,
|
||||||
startSuggestionTimer,
|
startSuggestionTimer,
|
||||||
Stretch{1}}};
|
Stretch{1}}};
|
||||||
|
|
||||||
auto hintTriggerSettings = Column{
|
|
||||||
Row{hintCharThreshold, hintHideTimeout, Stretch{1}},
|
|
||||||
Row{hintTriggerKey, Stretch{1}}};
|
|
||||||
|
|
||||||
return Column{Row{Stretch{1}, resetToDefaults},
|
return Column{Row{Stretch{1}, resetToDefaults},
|
||||||
Space{8},
|
Space{8},
|
||||||
Group{title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
Group{title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
||||||
Column{Group{title(Tr::tr("General Settings")), generalSettings},
|
Column{Group{title(Tr::tr("General Settings")), generalSettings},
|
||||||
Space{8},
|
Space{8},
|
||||||
Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings},
|
Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}},
|
||||||
Space{8},
|
|
||||||
Group{title(Tr::tr("Hint-based Trigger Mode")), hintTriggerSettings}}},
|
|
||||||
Space{8},
|
Space{8},
|
||||||
Group{title(Tr::tr("General Parameters")),
|
Group{title(Tr::tr("General Parameters")),
|
||||||
Column{
|
Column{
|
||||||
@@ -460,6 +486,9 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
|||||||
resetAspect(useOpenFilesContext);
|
resetAspect(useOpenFilesContext);
|
||||||
resetAspect(modelOutputHandler);
|
resetAspect(modelOutputHandler);
|
||||||
resetAspect(completionTriggerMode);
|
resetAspect(completionTriggerMode);
|
||||||
|
resetAspect(completionMode);
|
||||||
|
resetAspect(smartContextTrigger);
|
||||||
|
resetAspect(respectQtcPopup);
|
||||||
resetAspect(hintCharThreshold);
|
resetAspect(hintCharThreshold);
|
||||||
resetAspect(hintHideTimeout);
|
resetAspect(hintHideTimeout);
|
||||||
resetAspect(hintTriggerKey);
|
resetAspect(hintTriggerKey);
|
||||||
@@ -469,6 +498,21 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CodeCompletionSettings::migrateCompletionMode()
|
||||||
|
{
|
||||||
|
auto *qtcSettings = Core::ICore::settings();
|
||||||
|
if (!qtcSettings || qtcSettings->contains(Constants::CC_COMPLETION_MODE))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString oldMode = completionTriggerMode.stringValue();
|
||||||
|
if (oldMode.startsWith("Hint"))
|
||||||
|
completionMode.setStringValue("Manual");
|
||||||
|
else
|
||||||
|
completionMode.setStringValue("Automatic");
|
||||||
|
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
|
||||||
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const
|
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const
|
||||||
{
|
{
|
||||||
QString result = userMessageTemplateForCC();
|
QString result = userMessageTemplateForCC();
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public:
|
|||||||
Utils::BoolAspect multiLineCompletion{this};
|
Utils::BoolAspect multiLineCompletion{this};
|
||||||
Utils::SelectionAspect modelOutputHandler{this};
|
Utils::SelectionAspect modelOutputHandler{this};
|
||||||
Utils::SelectionAspect completionTriggerMode{this};
|
Utils::SelectionAspect completionTriggerMode{this};
|
||||||
|
Utils::SelectionAspect completionMode{this};
|
||||||
|
Utils::BoolAspect smartContextTrigger{this};
|
||||||
|
Utils::BoolAspect respectQtcPopup{this};
|
||||||
|
|
||||||
Utils::IntegerAspect startSuggestionTimer{this};
|
Utils::IntegerAspect startSuggestionTimer{this};
|
||||||
Utils::IntegerAspect autoCompletionCharThreshold{this};
|
Utils::IntegerAspect autoCompletionCharThreshold{this};
|
||||||
@@ -78,6 +81,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
void resetSettingsToDefaults();
|
void resetSettingsToDefaults();
|
||||||
|
void migrateCompletionMode();
|
||||||
};
|
};
|
||||||
|
|
||||||
CodeCompletionSettings &codeCompletionSettings();
|
CodeCompletionSettings &codeCompletionSettings();
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ const char СС_START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
|
|||||||
const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
|
const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
|
||||||
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
|
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
|
||||||
const char CC_COMPLETION_TRIGGER_MODE[] = "QodeAssist.ccCompletionTriggerMode";
|
const char CC_COMPLETION_TRIGGER_MODE[] = "QodeAssist.ccCompletionTriggerMode";
|
||||||
|
const char CC_COMPLETION_MODE[] = "QodeAssist.ccCompletionMode";
|
||||||
|
const char CC_SMART_CONTEXT_TRIGGER[] = "QodeAssist.ccSmartContextTrigger";
|
||||||
|
const char CC_RESPECT_QTC_POPUP[] = "QodeAssist.ccRespectQtcPopup";
|
||||||
const char CC_HINT_CHAR_THRESHOLD[] = "QodeAssist.ccHintCharThreshold";
|
const char CC_HINT_CHAR_THRESHOLD[] = "QodeAssist.ccHintCharThreshold";
|
||||||
const char CC_HINT_HIDE_TIMEOUT[] = "QodeAssist.ccHintHideTimeout";
|
const char CC_HINT_HIDE_TIMEOUT[] = "QodeAssist.ccHintHideTimeout";
|
||||||
const char CC_HINT_TRIGGER_KEY[] = "QodeAssist.ccHintTriggerKey";
|
const char CC_HINT_TRIGGER_KEY[] = "QodeAssist.ccHintTriggerKey";
|
||||||
|
|||||||
Reference in New Issue
Block a user