feat: add support code completion via acp

This commit is contained in:
Petr Mironychev
2026-07-22 10:27:33 +02:00
parent 442263a6a7
commit 583b5de14c
34 changed files with 2310 additions and 1414 deletions

View File

@@ -47,7 +47,6 @@ add_subdirectory(sources)
add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS
QtCreator::Core
QtCreator::LanguageClient
QtCreator::TextEditor
QtCreator::ProjectExplorer
QtCreator::CppEditor
@@ -78,10 +77,12 @@ add_qtc_plugin(QodeAssist
sources/plugin/UpdateStatusWidget.hpp sources/plugin/UpdateStatusWidget.cpp
sources/llmcore/ContextData.hpp
sources/llmcore/RequestType.hpp
sources/completion/LLMClientInterface.hpp sources/completion/LLMClientInterface.cpp
sources/completion/LSPCompletion.hpp
sources/completion/CompletionEngine.hpp
sources/completion/FimCompletionEngine.hpp sources/completion/FimCompletionEngine.cpp
sources/completion/AgenticCompletionEngine.hpp sources/completion/AgenticCompletionEngine.cpp
sources/completion/AcpCompletionEngine.hpp sources/completion/AcpCompletionEngine.cpp
sources/completion/CodeCompletionController.hpp sources/completion/CodeCompletionController.cpp
sources/completion/LLMSuggestion.hpp sources/completion/LLMSuggestion.cpp
sources/completion/QodeAssistClient.hpp sources/completion/QodeAssistClient.cpp
sources/completion/CodeHandler.hpp sources/completion/CodeHandler.cpp
sources/refactor/QuickRefactorHandler.hpp sources/refactor/QuickRefactorHandler.cpp
sources/refactor/RefactorContextHelper.hpp
@@ -141,12 +142,8 @@ add_qtc_plugin(QodeAssist
sources/chat/ChatEditorFactory.hpp sources/chat/ChatEditorFactory.cpp
sources/widgets/CompletionProgressHandler.hpp sources/widgets/CompletionProgressHandler.cpp
sources/widgets/CompletionErrorHandler.hpp sources/widgets/CompletionErrorHandler.cpp
sources/widgets/CompletionHintWidget.hpp sources/widgets/CompletionHintWidget.cpp
sources/widgets/CompletionHintHandler.hpp sources/widgets/CompletionHintHandler.cpp
sources/widgets/ProgressWidget.hpp sources/widgets/ProgressWidget.cpp
sources/widgets/ErrorWidget.hpp sources/widgets/ErrorWidget.cpp
sources/widgets/EditorChatButton.hpp sources/widgets/EditorChatButton.cpp
sources/widgets/EditorChatButtonHandler.hpp sources/widgets/EditorChatButtonHandler.cpp
sources/widgets/QuickRefactorDialog.hpp sources/widgets/QuickRefactorDialog.cpp
sources/widgets/CustomInstructionsManager.hpp sources/widgets/CustomInstructionsManager.cpp
sources/widgets/AddCustomInstructionDialog.hpp sources/widgets/AddCustomInstructionDialog.cpp
@@ -166,10 +163,12 @@ add_qtc_plugin(QodeAssist
sources/tools/ReadFileTool.hpp sources/tools/ReadFileTool.cpp
sources/tools/FileSearchUtils.hpp sources/tools/FileSearchUtils.cpp
sources/tools/TodoTool.hpp sources/tools/TodoTool.cpp
sources/tools/ProposeCompletionTool.hpp sources/tools/ProposeCompletionTool.cpp
sources/tools/ReadOriginalHistoryTool.hpp sources/tools/ReadOriginalHistoryTool.cpp
sources/tools/SkillTool.hpp sources/tools/SkillTool.cpp
sources/tools/EditorStateTools.hpp sources/tools/EditorStateTools.cpp
sources/mcp/AgentKnowledgeServer.hpp sources/mcp/AgentKnowledgeServer.cpp
sources/mcp/CompletionMcpServer.hpp sources/mcp/CompletionMcpServer.cpp
sources/mcp/McpServerManager.hpp sources/mcp/McpServerManager.cpp
sources/mcp/McpServerConnection.hpp sources/mcp/McpServerConnection.cpp
sources/mcp/McpClientsManager.hpp sources/mcp/McpClientsManager.cpp

37
docs/completion-modes.md Normal file
View File

@@ -0,0 +1,37 @@
# Code completion modes
QodeAssist produces an inline code-completion suggestion (ghost text you accept with Tab)
in one of four ways, chosen per setup in **Preferences → QodeAssist → Code Completion →
Completion mode**. All four share the same trigger heuristics, ghost-text rendering, and
Tab / word / line accept — they differ only in *how* the suggestion is produced. Pick the
one that matches your model's capability.
| Mode | What it does | Best for |
|---|---|---|
| **Classic FIM** | One fill-in-the-middle request to the configured provider, using the code around the cursor. | Weak or older local models; the lowest-latency option; the default. |
| **FIM + context** | The same FIM request, enriched with semantic context read from Qt Creator's built-in code model (C++ class outline and referenced declarations; QML sibling components). Chat-type templates only. | Capable models where extra context improves the completion and the added prompt size is affordable. |
| **Agentic (local tools)** | A tool-capable provider model runs the in-process tool loop, gathers its own context, and returns the completion by calling the `propose_completion` tool. | Strong tool-calling models, on demand. Multi-round and slow — **manual trigger only**. |
| **Agentic (ACP agent)** | An external ACP agent (chosen below the mode selector) does the same over the QodeAssist MCP server. | Using an external coding agent as the completion engine. Manual trigger only; needs an agent selected. |
## Notes and requirements
- **Default and migration.** Existing setups open as **Classic FIM** with unchanged
behavior — the mode is a new setting with a behavior-preserving default, and your
connection, model, template, and API-key settings are untouched.
- **FIM + context** only enriches C++ and QML files, and only when the active template is a
chat-type template. The built-in code model can lag unsaved edits, so the added context
is a hint, not a guarantee; the request still runs for unsupported languages, just without
the extra context. (Qt Creator's built-in `CPlusPlus` model is used, not clangd's AST,
which is not reachable by plugins.)
- **Agentic modes are manual-trigger only.** Because a tool loop takes seconds and many
tokens, the agentic modes are *not* fired automatically while you type — invoke them with
the **Request QodeAssist Suggestion** shortcut (default `Ctrl+Alt+Q`, reconfigurable in
Preferences → Keyboard). They need a tool-capable provider (local) and a chat-type
template.
- **Agentic (ACP agent)** additionally needs an agent id from the Agents catalogue entered
in **ACP completion agent id**, and the QodeAssist MCP server running. With no agent
selected, the mode is disabled — it never falls back to a FIM request; a manual trigger
reports the missing configuration instead.
See also: [file-context.md](file-context.md) for how the plain-text context window is
assembled, and [acp-agents.md](acp-agents.md) for configuring ACP agents.

View File

@@ -0,0 +1,427 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/AcpCompletionEngine.hpp"
#include <chrono>
#include <LLMQore/AcpClient.hpp>
#include <LLMQore/CallbackPermissionProvider.hpp>
#include "acp/AgentSpawn.hpp"
#include "context/DocumentContextReader.hpp"
#include "logger/Logger.hpp"
#include "settings/SettingsConstants.hpp"
namespace QodeAssist {
namespace {
void upsertEnv(QList<Acp::AgentEnvVariable> &env, const QString &name, const QString &value)
{
for (auto &variable : env) {
if (variable.name == name) {
variable.value = value;
return;
}
}
env.append({name, value});
}
LLMQore::Acp::RequestPermissionResult autoAllow(
const QString &,
const LLMQore::Acp::ToolCall &toolCall,
const QList<LLMQore::Acp::PermissionOption> &options)
{
QStringList optionDump;
for (const auto &option : options)
optionDump.append(QString("%1(%2)").arg(option.optionId, option.kind));
const LLMQore::Acp::PermissionOption *chosen = nullptr;
for (const auto &option : options) {
if (option.kind == QLatin1String(LLMQore::Acp::PermissionOptionKind::AllowOnce)) {
chosen = &option;
break;
}
if (!chosen && option.kind == QLatin1String(LLMQore::Acp::PermissionOptionKind::AllowAlways))
chosen = &option;
}
if (!chosen && !options.isEmpty())
chosen = &options.first();
LOG_MESSAGE(
QString("ACP completion: permission requested for tool '%1' (kind=%2); options=[%3]; %4")
.arg(
toolCall.title,
toolCall.kind,
optionDump.join(QStringLiteral(", ")),
chosen ? QString("auto-allowing '%1'").arg(chosen->optionId)
: QStringLiteral("cancelling (no option offered)")));
return chosen ? LLMQore::Acp::RequestPermissionResult::selected(chosen->optionId)
: LLMQore::Acp::RequestPermissionResult::cancelled();
}
} // namespace
AcpCompletionEngine::AcpCompletionEngine(
const Settings::CodeCompletionSettings &completeSettings,
const Settings::GeneralSettings &generalSettings,
AgentResolver agentResolver,
Context::IDocumentReader &documentReader,
Tools::ProposeCompletionTool *proposeTool,
QObject *parent)
: CompletionEngine(parent)
, m_completeSettings(completeSettings)
, m_generalSettings(generalSettings)
, m_agentResolver(std::move(agentResolver))
, m_documentReader(documentReader)
, m_proposeTool(proposeTool)
, m_clientFactory(&Acp::spawnAgent)
, m_completionServer(new Mcp::CompletionMcpServer(this))
, m_permissions(new LLMQore::Acp::CallbackPermissionProvider(&autoAllow, this))
{
if (m_proposeTool) {
connect(
m_proposeTool,
&Tools::ProposeCompletionTool::completionProposed,
this,
&AcpCompletionEngine::handleCompletionProposed);
}
}
void AcpCompletionEngine::setClientFactory(ClientFactory factory)
{
if (factory)
m_clientFactory = std::move(factory);
}
bool AcpCompletionEngine::hasConfiguredAgent() const
{
const QString agentId = m_completeSettings.completionAgentId();
if (agentId.isEmpty() || !m_agentResolver)
return false;
return m_agentResolver(agentId).has_value();
}
void AcpCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
if (m_active) {
const auto stale = *m_active;
m_active.reset();
if (m_client && !m_sessionId.isEmpty())
m_client->cancel(m_sessionId);
emit completionFailed(stale.requestId, QStringLiteral("Superseded by a newer request"));
}
const QString agentId = m_completeSettings.completionAgentId();
if (agentId.isEmpty()) {
emit completionFailed(
requestId, QStringLiteral("Agentic (ACP agent) completion has no agent selected"));
return;
}
if (!m_agentResolver || !m_agentResolver(agentId).has_value()) {
emit completionFailed(
requestId, QString("ACP completion agent '%1' is not in the catalogue").arg(agentId));
return;
}
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
emit completionFailed(
requestId, QString("Document is not available: %1").arg(context.filePath));
return;
}
Context::DocumentContextReader reader(
documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
const auto ctx = reader.prepareContext(context.line, context.column, m_completeSettings);
const QString codeContext = ctx.prefix.value_or("") + "<cursor>" + ctx.suffix.value_or("");
m_active
= ActiveRequest{requestId, documentInfo.filePath, context.line, context.column, codeContext};
LOG_MESSAGE(
QString("ACP completion: request %1 for %2 (line %3, col %4), agent '%5'")
.arg(QString::number(requestId), documentInfo.filePath)
.arg(context.line)
.arg(context.column)
.arg(agentId));
if (m_boundAgentId != agentId) {
if (!m_boundAgentId.isEmpty())
LOG_MESSAGE(
QString("ACP completion: selected agent changed %1 -> %2, releasing client")
.arg(m_boundAgentId, agentId));
releaseClient();
}
m_boundAgentId = agentId;
ensureClientAndPrompt();
}
void AcpCompletionEngine::ensureClientAndPrompt()
{
if (m_client && m_sessionReady) {
LOG_MESSAGE(
QString("ACP completion: reusing live session %1, sending a new prompt")
.arg(m_sessionId));
sendPrompt();
return;
}
if (m_client) {
LOG_MESSAGE("ACP completion: client is still establishing its session, will prompt once ready");
return;
}
const auto agent = m_agentResolver ? m_agentResolver(m_boundAgentId) : std::nullopt;
if (!agent) {
failActive(QString("ACP completion agent '%1' vanished from the catalogue").arg(m_boundAgentId));
return;
}
Acp::AgentDefinition launchAgent = *agent;
const QString model = m_generalSettings.ccModel();
if (!model.isEmpty()) {
upsertEnv(launchAgent.distribution.env, QStringLiteral("ANTHROPIC_MODEL"), model);
LOG_MESSAGE(
QString("ACP completion: injecting ANTHROPIC_MODEL=%1 into agent env").arg(model));
}
const QString cwd = Acp::agentWorkingDirectory();
LOG_MESSAGE(
QString("ACP completion: spawning agent '%1' (%2) in %3")
.arg(launchAgent.id, launchAgent.name, cwd));
const Acp::AgentProcess process = m_clientFactory(launchAgent, cwd, this);
m_client = process.client;
if (!m_client) {
failActive(QStringLiteral("This agent has no launchable distribution"));
return;
}
m_client->setPermissionProvider(m_permissions);
const int generation = ++m_clientGeneration;
connect(m_client, &LLMQore::Acp::AcpClient::errorOccurred, this, [this, generation](const QString &error) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: agent error: %1").arg(error));
failActive(error);
releaseClient();
});
connect(m_client, &LLMQore::Acp::AcpClient::disconnected, this, [this, generation]() {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE("ACP completion: agent disconnected");
m_sessionReady = false;
m_sessionId.clear();
});
connect(m_client, &LLMQore::Acp::AcpClient::agentStderr, this, [this, generation](const QString &line) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion [agent stderr]: %1").arg(line));
});
connect(
m_client,
&LLMQore::Acp::AcpClient::toolCallStarted,
this,
[this, generation](const QString &, const LLMQore::Acp::ToolCall &call) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(
QString("ACP completion: agent tool call started: title='%1' kind=%2 status=%3")
.arg(call.title, call.kind, call.status));
});
connect(
m_client,
&LLMQore::Acp::AcpClient::toolCallUpdated,
this,
[this, generation](const QString &, const LLMQore::Acp::ToolCall &call) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(
QString("ACP completion: agent tool call updated: title='%1' kind=%2 status=%3")
.arg(call.title, call.kind, call.status));
});
LOG_MESSAGE("ACP completion: connecting and initializing the agent");
m_client->connectAndInitialize(std::chrono::seconds(30))
.then(this, [this, generation, cwd](const LLMQore::Acp::InitializeResult &info) {
if (generation != m_clientGeneration || !m_client)
return;
const bool http = info.agentCapabilities.mcpCapabilities.http;
LOG_MESSAGE(
QString("ACP completion: agent initialized (protocol %1, MCP http=%2 sse=%3)")
.arg(QString::number(info.protocolVersion),
http ? QStringLiteral("true") : QStringLiteral("false"),
info.agentCapabilities.mcpCapabilities.sse ? QStringLiteral("true")
: QStringLiteral("false")));
if (!http) {
LOG_MESSAGE(
"ACP completion: WARNING — this agent does not advertise HTTP MCP support, so "
"it will likely ignore the QodeAssist MCP server and never call "
"propose_completion. Use an agent that accepts HTTP MCP servers.");
}
const QString mcpUrl = m_completionServer->start(m_proposeTool);
if (mcpUrl.isEmpty()) {
failActive(QStringLiteral(
"The QodeAssist completion MCP server could not start; ACP completion needs it"));
releaseClient();
return;
}
LLMQore::Acp::NewSessionParams params;
params.cwd = cwd;
params.mcpServers
= {LLMQore::Acp::McpServer::http(Mcp::CompletionMcpServer::serverName(), mcpUrl)};
LOG_MESSAGE(
QString("ACP completion: creating session, offering MCP server '%1' at %2")
.arg(Mcp::CompletionMcpServer::serverName(), mcpUrl));
m_client->newSession(params, std::chrono::seconds(30))
.then(this, [this, generation](const LLMQore::Acp::NewSessionResult &result) {
if (generation != m_clientGeneration || !m_client)
return;
m_sessionId = result.sessionId;
m_sessionReady = true;
LOG_MESSAGE(QString("ACP completion: session %1 established").arg(m_sessionId));
sendPrompt();
})
.onFailed(this, [this, generation](const std::exception &e) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: session/new failed: %1")
.arg(QString::fromUtf8(e.what())));
failActive(QString::fromUtf8(e.what()));
releaseClient();
});
})
.onFailed(this, [this, generation](const std::exception &e) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: initialize failed: %1")
.arg(QString::fromUtf8(e.what())));
failActive(QString::fromUtf8(e.what()));
releaseClient();
});
}
void AcpCompletionEngine::sendPrompt()
{
if (!m_active || !m_client || m_sessionId.isEmpty())
return;
const int generation = m_clientGeneration;
const quint64 activeId = m_active->requestId;
const QString text = promptText();
LOG_MESSAGE(
QString("ACP completion: prompting session %1 for request %2")
.arg(m_sessionId, QString::number(activeId)));
LOG_MESSAGE(QString("ACP completion: prompt text:\n%1").arg(text));
m_client->prompt(m_sessionId, {LLMQore::Acp::ContentBlock::makeText(text)})
.then(this, [this, generation, activeId](const LLMQore::Acp::PromptResult &result) {
if (generation != m_clientGeneration || !m_active || m_active->requestId != activeId)
return;
LOG_MESSAGE(
QString("ACP completion: prompt for request %1 finished with stopReason '%2' but "
"no proposal arrived")
.arg(QString::number(activeId), result.stopReason));
m_active.reset();
emit completionFailed(
activeId,
QStringLiteral("The agent finished without proposing a completion"));
})
.onFailed(this, [this, generation, activeId](const std::exception &e) {
if (generation != m_clientGeneration || !m_active || m_active->requestId != activeId)
return;
m_active.reset();
LOG_MESSAGE(QString("ACP completion failed: %1").arg(QString::fromUtf8(e.what())));
emit completionFailed(activeId, QString::fromUtf8(e.what()));
});
}
QString AcpCompletionEngine::promptText() const
{
if (!m_active)
return {};
return QString(
"Complete the code at the <cursor> marker in the snippet below. Be fast: do NOT "
"use any other tools, do NOT read files, run commands, or search — work only "
"from this snippet.\n\n"
"<code_context>\n%1\n</code_context>\n\n"
"First work out the exact code that should replace <cursor> (just the "
"continuation — no explanations, no code fences, and do not repeat the code "
"already before or after the cursor). Then make ONE call to propose_completion "
"with that code as the `text` argument. The `text` must be the real completion "
"and must never be empty. file=%2, line=%3, column=%4.")
.arg(m_active->codeContext)
.arg(m_active->filePath)
.arg(m_active->line)
.arg(m_active->column);
}
void AcpCompletionEngine::cancel(quint64 requestId)
{
if (!m_active || m_active->requestId != requestId)
return;
m_active.reset();
if (m_client && !m_sessionId.isEmpty())
m_client->cancel(m_sessionId);
}
void AcpCompletionEngine::handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text)
{
Q_UNUSED(line)
Q_UNUSED(column)
LOG_MESSAGE(
QString("ACP completion: propose_completion called for '%1' (%2 chars)")
.arg(filePath, QString::number(text.size())));
if (!m_active) {
LOG_MESSAGE("ACP completion: ignoring proposal — no completion request is active");
return;
}
if (m_active->filePath != filePath) {
LOG_MESSAGE(
QString("ACP completion: proposal path '%1' differs from the requested '%2'; "
"accepting anyway (single active request)")
.arg(filePath, m_active->filePath));
}
const quint64 externalId = m_active->requestId;
m_active.reset();
LOG_MESSAGE(
QString("ACP completion: rendering proposal for request %1").arg(QString::number(externalId)));
emit completionReady(externalId, text);
}
void AcpCompletionEngine::failActive(const QString &error)
{
if (!m_active)
return;
const auto stale = *m_active;
m_active.reset();
LOG_MESSAGE(QString("ACP completion failed: %1").arg(error));
emit completionFailed(stale.requestId, error);
}
void AcpCompletionEngine::releaseClient()
{
++m_clientGeneration;
m_sessionReady = false;
m_sessionId.clear();
if (m_client) {
m_client->shutdown();
m_client->deleteLater();
m_client = nullptr;
}
if (m_completionServer)
m_completionServer->stop();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <functional>
#include <optional>
#include <QString>
#include <LLMQore/AcpPermissionProvider.hpp>
#include "acp/AgentDefinition.hpp"
#include "acp/AgentSpawn.hpp"
#include "completion/CompletionEngine.hpp"
#include "context/IDocumentReader.hpp"
#include "mcp/CompletionMcpServer.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "tools/ProposeCompletionTool.hpp"
namespace QodeAssist {
class AcpCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
using ClientFactory = std::function<
Acp::AgentProcess(const Acp::AgentDefinition &agent, const QString &cwd, QObject *parent)>;
using AgentResolver = std::function<std::optional<Acp::AgentDefinition>(const QString &id)>;
AcpCompletionEngine(
const Settings::CodeCompletionSettings &completeSettings,
const Settings::GeneralSettings &generalSettings,
AgentResolver agentResolver,
Context::IDocumentReader &documentReader,
Tools::ProposeCompletionTool *proposeTool,
QObject *parent = nullptr);
void setClientFactory(ClientFactory factory);
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
bool hasConfiguredAgent() const;
private slots:
void handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
int line = 0;
int column = 0;
QString codeContext;
};
void ensureClientAndPrompt();
void sendPrompt();
void failActive(const QString &error);
void releaseClient();
QString promptText() const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
AgentResolver m_agentResolver;
Context::IDocumentReader &m_documentReader;
Tools::ProposeCompletionTool *m_proposeTool = nullptr;
ClientFactory m_clientFactory;
Mcp::CompletionMcpServer *m_completionServer = nullptr;
LLMQore::Acp::AcpPermissionProvider *m_permissions = nullptr;
LLMQore::Acp::AcpClient *m_client = nullptr;
QString m_boundAgentId;
QString m_sessionId;
int m_clientGeneration = 0;
bool m_sessionReady = false;
std::optional<ActiveRequest> m_active;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,249 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/AgenticCompletionEngine.hpp"
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QUrl>
#include "context/DocumentContextReader.hpp"
#include "llmcore/ContextData.hpp"
#include "logger/Logger.hpp"
#include "settings/ToolsSettings.hpp"
namespace QodeAssist {
AgenticCompletionEngine::AgenticCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger,
QObject *parent)
: CompletionEngine(parent)
, m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
{
}
AgenticCompletionEngine::~AgenticCompletionEngine()
{
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.constBegin(); it != requests.constEnd(); ++it) {
if (it.value().provider)
it.value().provider->cancelRequest(it.key());
}
}
void AgenticCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
const QString error = QString("Document is not available: %1").arg(context.filePath);
LOG_MESSAGE("Error: " + error);
emit completionFailed(requestId, error);
return;
}
const auto providerName = m_generalSettings.ccProvider();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
const QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (!provider->capabilities().testFlag(Providers::ProviderCapability::Tools)) {
const QString error
= QString("Agentic completion needs tool calling, which provider %1 does not support")
.arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
auto promptTemplate = m_promptProvider->getTemplateByName(m_generalSettings.ccTemplate());
if (!promptTemplate) {
const QString error
= QString("No template found with name: %1").arg(m_generalSettings.ccTemplate());
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (promptTemplate->type() != Templates::TemplateType::Chat) {
const QString error
= QString("Agentic completion needs a chat-type template; %1 is FIM-only")
.arg(promptTemplate->name());
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (auto *toolsManager = provider->toolsManager()) {
auto *proposeTool = qobject_cast<Tools::ProposeCompletionTool *>(
toolsManager->tool(Tools::ProposeCompletionTool::toolId()));
if (!proposeTool) {
proposeTool = new Tools::ProposeCompletionTool();
toolsManager->addTool(proposeTool);
}
connect(
proposeTool,
&Tools::ProposeCompletionTool::completionProposed,
this,
&AgenticCompletionEngine::handleCompletionProposed,
Qt::UniqueConnection);
}
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
auto updatedContext = reader.prepareContext(context.line, context.column, m_completeSettings);
QString systemPrompt = m_completeSettings.systemPromptForNonFimModels();
systemPrompt.append(
QString("\n\nYou are completing code at %1, zero-based line %2, column %3. "
"You may call the read-only project tools to gather context. When you know the "
"completion, call the propose_completion tool exactly once with the completion "
"text for that position; the text is shown to the user as an inline suggestion. "
"Do not answer in prose and do not edit any files.")
.arg(documentInfo.filePath)
.arg(context.line)
.arg(context.column));
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
updatedContext.systemPrompt = systemPrompt;
QString userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
QJsonObject payload{{"model", m_generalSettings.ccModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
true,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&AgenticCompletionEngine::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&AgenticCompletionEngine::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto llmRequestId = provider->sendRequest(
QUrl(m_generalSettings.ccUrl()), payload, promptTemplate->endpoint());
m_activeRequests.insert(llmRequestId, {requestId, documentInfo.filePath, provider});
m_performanceLogger.startTimeMeasurement(llmRequestId);
}
void AgenticCompletionEngine::cancel(quint64 requestId)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().requestId != requestId)
continue;
const auto llmRequestId = it.key();
auto *provider = it.value().provider;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(llmRequestId);
if (provider)
provider->cancelRequest(llmRequestId);
return;
}
}
void AgenticCompletionEngine::handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text)
{
Q_UNUSED(line)
Q_UNUSED(column)
auto match = m_activeRequests.end();
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().filePath == filePath) {
match = it;
break;
}
}
if (match == m_activeRequests.end() && m_activeRequests.size() == 1)
match = m_activeRequests.begin();
if (match == m_activeRequests.end()) {
LOG_MESSAGE(
QString("Ignoring completion proposal for %1: no completion request in flight")
.arg(filePath));
return;
}
const quint64 externalId = match.value().requestId;
const auto llmRequestId = match.key();
m_activeRequests.erase(match);
m_performanceLogger.endTimeMeasurement(llmRequestId);
emit completionReady(externalId, text);
}
void AgenticCompletionEngine::handleFullResponse(
const ::LLMQore::RequestID &requestId, const QString &fullText)
{
Q_UNUSED(fullText)
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
const QString error
= QStringLiteral("The model finished without proposing a completion");
LOG_MESSAGE(QString("Agentic completion request %1: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
void AgenticCompletionEngine::handleRequestFailed(
const ::LLMQore::RequestID &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
LOG_MESSAGE(QString("Agentic completion request %1 failed: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
} // namespace QodeAssist

View File

@@ -0,0 +1,61 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QHash>
#include "completion/CompletionEngine.hpp"
#include "context/IDocumentReader.hpp"
#include "logger/IRequestPerformanceLogger.hpp"
#include "providers/IProviderRegistry.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "templates/IPromptProvider.hpp"
#include "tools/ProposeCompletionTool.hpp"
namespace QodeAssist {
class AgenticCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
AgenticCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger,
QObject *parent = nullptr);
~AgenticCompletionEngine() override;
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
private slots:
void handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text);
void handleFullResponse(const ::LLMQore::RequestID &requestId, const QString &fullText);
void handleRequestFailed(const ::LLMQore::RequestID &requestId, const QString &error);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
Providers::Provider *provider = nullptr;
};
const Settings::GeneralSettings &m_generalSettings;
const Settings::CodeCompletionSettings &m_completeSettings;
Providers::IProviderRegistry &m_providerRegistry;
Templates::IPromptProvider *m_promptProvider = nullptr;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QHash<::LLMQore::RequestID, ActiveRequest> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -24,30 +24,26 @@
* Additional attribution terms under GPLv3 §7(b) apply see LICENSE
*/
#include "completion/QodeAssistClient.hpp"
#include "completion/CodeCompletionController.hpp"
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/projectmanager.h>
#include "completion/LLMClientInterface.hpp"
#include "completion/LLMSuggestion.hpp"
#include "refactor/RefactorContextHelper.hpp"
#include "refactor/RefactorSuggestion.hpp"
#include "refactor/RefactorSuggestionHoverHandler.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include "refactor/RefactorContextHelper.hpp"
#include "settings/SettingsConstants.hpp"
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
using namespace Utils;
using namespace ProjectExplorer;
@@ -56,11 +52,6 @@ using namespace Core;
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('_');
@@ -133,23 +124,32 @@ bool isAfterEagerTrigger(const QTextCursor &cursor)
|| c == QLatin1Char(':') || c == QLatin1Char('>');
}
bool isManualMode()
bool isManualTriggerMode()
{
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
return Settings::codeCompletionSettings().triggerMode.stringValue() == "Manual";
}
bool isAgenticMode()
{
const QString mode = Settings::codeCompletionSettings().completionMode.stringValue();
return mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_LOCAL)
|| mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_ACP);
}
} // anonymous namespace
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
CodeCompletionController::CodeCompletionController(
CompletionEngine *fimEngine,
CompletionEngine *agenticEngine,
CompletionEngine *acpEngine,
Context::ContextManager &contextManager,
QObject *parent)
: QObject(parent)
, m_fimEngine(fimEngine)
, m_agenticEngine(agenticEngine)
, m_acpEngine(acpEngine)
, m_contextManager(contextManager)
, m_recentCharCount(0)
{
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
start();
setupConnections();
m_typingTimer.start();
@@ -158,30 +158,33 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
QodeAssistClient::~QodeAssistClient()
CodeCompletionController::~CodeCompletionController()
{
cleanupConnections();
delete m_refactorHoverHandler;
delete m_refactorWidgetHandler;
}
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
void CodeCompletionController::openDocument(TextEditor::TextDocument *document)
{
if (m_documentConnections.contains(document))
return;
auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project))
return;
Client::openDocument(document);
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
for (auto *editor : editors) {
if (auto *widget = editor->editorWidget()) {
widget->addHoverHandler(m_refactorHoverHandler);
widget->installEventFilter(this);
if (!m_hoverHandlerWidgets.contains(widget))
m_hoverHandlerWidgets.append(widget);
}
}
connect(
auto connection = connect(
document,
&TextDocument::contentsChangedWithPosition,
this,
@@ -189,7 +192,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (!Settings::codeCompletionSettings().autoCompletion())
return;
if (isManualMode())
if (isManualTriggerMode() || isAgenticMode())
return;
auto project = ProjectManager::projectForFile(document->filePath());
@@ -243,42 +246,47 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
handleAutoRequestTrigger(widget);
});
m_documentConnections.insert(document, connection);
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
void CodeCompletionController::closeDocument(TextEditor::TextDocument *document)
{
return isEnabled(project);
auto it = m_documentConnections.find(document);
if (it == m_documentConnections.end())
return;
disconnect(it.value());
m_documentConnections.erase(it);
}
void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::requestCompletions(TextEditor::TextEditorWidget *editor)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_contextManager.ignoreManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
auto *engine = activeEngine();
if (!engine)
return;
MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return;
cancelRunningRequest(editor);
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
if (editor) {
@@ -287,16 +295,22 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
});
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return);
handleCompletions(response, editor);
});
m_runningRequests[editor] = request;
sendMessage(request);
const int position = cursor.mainCursor().position();
const quint64 requestId = ++m_lastRequestId;
m_runningRequests[editor] = {requestId, position, engine};
m_requestEditors[requestId] = editor;
const QTextBlock block = editor->document()->findBlock(position);
CompletionContext context;
context.filePath = editor->textDocument()->filePath().toUrlishString();
context.line = block.blockNumber();
context.column = position - block.position();
engine->request(requestId, context);
}
void QodeAssistClient::requestQuickRefactor(
void CodeCompletionController::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
@@ -304,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_contextManager.ignoreManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
@@ -318,7 +331,7 @@ void QodeAssistClient::requestQuickRefactor(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
&CodeCompletionController::handleRefactoringResult);
}
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
@@ -331,7 +344,7 @@ void QodeAssistClient::requestQuickRefactor(
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
if (m_runningRequests.contains(editor)) {
if (Settings::codeCompletionSettings().cancelOnInput())
@@ -362,25 +375,74 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
}
void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
void CodeCompletionController::handleCompletionReady(quint64 requestId, const QString &text)
{
auto editorIt = m_requestEditors.find(requestId);
if (editorIt == m_requestEditors.end())
return;
const QPointer<TextEditorWidget> editor = editorIt.value();
m_requestEditors.erase(editorIt);
if (!editor) {
dropRunningRequestById(requestId);
return;
}
auto runningIt = m_runningRequests.find(editor);
if (runningIt == m_runningRequests.end() || runningIt.value().id != requestId)
return;
const int requestPosition = runningIt.value().position;
m_runningRequests.erase(runningIt);
m_progressHandler.hideProgress();
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
if (response.error()) {
log(*response.error());
m_errorHandler
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
applyCompletion(editor, text, requestPosition);
}
void CodeCompletionController::handleCompletionFailed(quint64 requestId, const QString &error)
{
auto editorIt = m_requestEditors.find(requestId);
if (editorIt == m_requestEditors.end())
return;
const QPointer<TextEditorWidget> editor = editorIt.value();
m_requestEditors.erase(editorIt);
LOG_MESSAGE(QString("Code completion failed: %1").arg(error));
if (!editor) {
dropRunningRequestById(requestId);
return;
}
int requestPosition = -1;
if (const auto requestParams = m_runningRequests.take(editor).params())
requestPosition = requestParams->position().toPositionInDocument(editor->document());
auto runningIt = m_runningRequests.find(editor);
if (runningIt == m_runningRequests.end() || runningIt.value().id != requestId)
return;
m_runningRequests.erase(runningIt);
m_progressHandler.hideProgress();
m_errorHandler.showError(editor, tr("Code completion failed: %1").arg(error));
}
void CodeCompletionController::dropRunningRequestById(quint64 requestId)
{
for (auto it = m_runningRequests.begin(); it != m_runningRequests.end(); ++it) {
if (it.value().id == requestId) {
m_runningRequests.erase(it);
return;
}
}
}
void CodeCompletionController::applyCompletion(
TextEditor::TextEditorWidget *editor, const QString &text, int requestPosition)
{
const MultiTextCursor cursors = editor->multiTextCursor();
if (cursors.hasMultipleCursors() || cursors.hasSelection())
return;
@@ -401,80 +463,51 @@ void QodeAssistClient::handleCompletions(
}
}
if (const std::optional<GetCompletionResponse> result = response.result()) {
auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty();
};
QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
QString completionText = text;
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completionText.chop(delta);
QList<Completion> matchedCompletions;
matchedCompletions.reserve(completions.size());
for (Completion &completion : completions) {
const LanguageServerProtocol::Range range = completion.range();
if (range.start().line() != range.end().line())
continue;
QString completionText = completion.text();
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completionText.chop(delta);
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest))
continue;
completionText = completionText.mid(typedSinceRequest.size());
if (completionText.isEmpty())
continue;
}
completion.setText(completionText);
matchedCompletions.append(completion);
}
if (matchedCompletions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest)) {
LOG_MESSAGE("Completion no longer matches the typed text");
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) {
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::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
completionText = completionText.mid(typedSinceRequest.size());
}
if (completionText.trimmed().isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
}
const int anchorPosition = typedSinceRequest.isEmpty() ? requestPosition : currentPosition;
const Text::Position anchor
= Text::Position::fromPositionInDocument(editor->document(), anchorPosition);
const TextSuggestion::Data suggestion{Text::Range{anchor, anchor}, anchor, completionText};
editor->insertSuggestion(
std::make_unique<LLMSuggestion>(QList<TextSuggestion::Data>{suggestion}, editor->document()));
}
void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
{
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
cancelRequest(it->id());
const quint64 requestId = it->id;
CompletionEngine *engine = it->engine;
m_runningRequests.erase(it);
m_requestEditors.remove(requestId);
m_progressHandler.hideProgress();
if (engine)
engine->cancel(requestId);
}
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
bool CodeCompletionController::isEnabled(ProjectExplorer::Project *project) const
{
if (!project)
return Settings::generalSettings().enableQodeAssist();
@@ -483,8 +516,33 @@ bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
return settings.isEnabled();
}
void QodeAssistClient::setupConnections()
CompletionEngine *CodeCompletionController::activeEngine() const
{
const QString mode = Settings::codeCompletionSettings().completionMode.stringValue();
if (mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_LOCAL))
return m_agenticEngine;
if (mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_ACP))
return m_acpEngine;
return m_fimEngine;
}
void CodeCompletionController::setupConnections()
{
for (CompletionEngine *engine : {m_fimEngine, m_agenticEngine, m_acpEngine}) {
if (!engine)
continue;
connect(
engine,
&CompletionEngine::completionReady,
this,
&CodeCompletionController::handleCompletionReady);
connect(
engine,
&CompletionEngine::completionFailed,
this,
&CodeCompletionController::handleCompletionFailed);
}
auto openDoc = [this](IDocument *document) {
if (auto *textDocument = qobject_cast<TextDocument *>(document))
openDocument(textDocument);
@@ -502,16 +560,26 @@ void QodeAssistClient::setupConnections()
openDoc(doc);
}
void QodeAssistClient::cleanupConnections()
void CodeCompletionController::cleanupConnections()
{
disconnect(m_documentOpenedConnection);
disconnect(m_documentClosedConnection);
for (auto it = m_documentConnections.begin(); it != m_documentConnections.end(); ++it)
disconnect(it.value());
m_documentConnections.clear();
for (const auto &widget : std::as_const(m_hoverHandlerWidgets)) {
if (widget)
widget->removeHoverHandler(m_refactorHoverHandler);
}
m_hoverHandlerWidgets.clear();
qDeleteAll(m_scheduledRequests);
m_scheduledRequests.clear();
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
void CodeCompletionController::handleRefactoringResult(const RefactorResult &result)
{
m_progressHandler.hideProgress();
@@ -534,7 +602,7 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
}
int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) {
displayRefactoringWidget(result);
} else {
@@ -542,12 +610,12 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
}
}
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
void CodeCompletionController::displayRefactoringSuggestion(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
Utils::Text::Range range{result.insertRange.begin, result.insertRange.end};
Utils::Text::Position pos = result.insertRange.begin;
int startPos = range.begin.toPositionInDocument(editorWidget->document());
int endPos = range.end.toPositionInDocument(editorWidget->document());
@@ -577,9 +645,7 @@ void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result
}
TextEditor::TextSuggestion::Data suggestionData{
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
pos,
result.newText};
Utils::Text::Range{result.insertRange.begin, result.insertRange.end}, pos, result.newText};
editorWidget->insertSuggestion(
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
@@ -599,20 +665,20 @@ void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
void CodeCompletionController::displayRefactoringWidget(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Range range{result.insertRange.begin, result.insertRange.end};
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
QString displayOriginal;
QString displayRefactored;
QString textToApply = result.newText;
if (ctx.isInsertion) {
bool isMultiline = result.newText.contains('\n');
if (isMultiline) {
displayOriginal = ctx.textBeforeCursor;
displayRefactored = ctx.textBeforeCursor + result.newText;
@@ -620,7 +686,7 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
}
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
textToApply = result.newText;
}
@@ -638,31 +704,30 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
m_refactorWidgetHandler->showRefactorWidget(
editorWidget, displayOriginal, displayRefactored, range,
ctx.contextBefore, ctx.contextAfter);
m_refactorWidgetHandler->setTextToApply(textToApply);
}
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &text)
void CodeCompletionController::applyRefactoringEdit(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text)
{
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(editor->document());
editCursor.beginEditBlock();
if (startPos == endPos) {
bool isMultiline = text.contains('\n');
editCursor.setPosition(startPos);
if (isMultiline) {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
} else {
editCursor.setPosition(startPos);
@@ -674,7 +739,7 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
editCursor.endEditBlock();
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
void CodeCompletionController::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
{
const QTextCursor cursor = widget->textCursor();
const auto &settings = Settings::codeCompletionSettings();
@@ -690,11 +755,11 @@ void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *wi
scheduleRequest(widget);
}
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
bool CodeCompletionController::eventFilter(QObject *watched, QEvent *event)
{
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
if (!editor)
return LanguageClient::Client::eventFilter(watched, event);
return QObject::eventFilter(watched, event);
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
@@ -719,7 +784,7 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
}
}
return LanguageClient::Client::eventFilter(watched, event);
return QObject::eventFilter(watched, event);
}
} // namespace QodeAssist

View File

@@ -5,31 +5,36 @@
#pragma once
#include <QElapsedTimer>
#include <QHash>
#include <QObject>
#include <QPointer>
#include "completion/LLMClientInterface.hpp"
#include "completion/LSPCompletion.hpp"
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include "completion/CompletionEngine.hpp"
#include "context/ContextManager.hpp"
#include "refactor/QuickRefactorHandler.hpp"
#include "refactor/RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include "templates/IPromptProvider.hpp"
#include "providers/IProviderRegistry.hpp"
namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client
class CodeCompletionController : public QObject
{
Q_OBJECT
public:
explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override;
public:
CodeCompletionController(
CompletionEngine *fimEngine,
CompletionEngine *agenticEngine,
CompletionEngine *acpEngine,
Context::ContextManager &contextManager,
QObject *parent = nullptr);
~CodeCompletionController() override;
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
@@ -39,23 +44,45 @@ protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
struct RunningRequest
{
quint64 id = 0;
int position = -1;
CompletionEngine *engine = nullptr;
};
void openDocument(TextEditor::TextDocument *document);
void closeDocument(TextEditor::TextDocument *document);
void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void handleCompletionReady(quint64 requestId, const QString &text);
void handleCompletionFailed(quint64 requestId, const QString &error);
void applyCompletion(
TextEditor::TextEditorWidget *editor, const QString &text, int requestPosition);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
void dropRunningRequestById(quint64 requestId);
bool isEnabled(ProjectExplorer::Project *project) const;
CompletionEngine *activeEngine() const;
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
void displayRefactoringSuggestion(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);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
CompletionEngine *m_fimEngine = nullptr;
CompletionEngine *m_agenticEngine = nullptr;
CompletionEngine *m_acpEngine = nullptr;
Context::ContextManager &m_contextManager;
quint64 m_lastRequestId = 0;
QHash<TextEditor::TextEditorWidget *, RunningRequest> m_runningRequests;
QHash<quint64, QPointer<TextEditor::TextEditorWidget>> m_requestEditors;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
QHash<TextEditor::TextDocument *, QMetaObject::Connection> m_documentConnections;
QList<QPointer<TextEditor::TextEditorWidget>> m_hoverHandlerWidgets;
QMetaObject::Connection m_documentOpenedConnection;
QMetaObject::Connection m_documentClosedConnection;
@@ -63,11 +90,9 @@ private:
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,34 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
namespace QodeAssist {
struct CompletionContext
{
QString filePath;
int line = 0;
int column = 0;
};
class CompletionEngine : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
virtual void request(quint64 requestId, const CompletionContext &context) = 0;
virtual void cancel(quint64 requestId) = 0;
signals:
void completionReady(quint64 requestId, const QString &text);
void completionFailed(quint64 requestId, const QString &error);
};
} // namespace QodeAssist

View File

@@ -0,0 +1,292 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/FimCompletionEngine.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonArray>
#include <QUrl>
#include "completion/CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "llmcore/ContextData.hpp"
#include "logger/Logger.hpp"
#include "settings/SettingsConstants.hpp"
namespace QodeAssist {
namespace {
constexpr int kSemanticContextTokenBudget = 1000;
}
FimCompletionEngine::FimCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
Context::ContextManager &contextManager,
IRequestPerformanceLogger &performanceLogger,
Context::ICompletionEnricher *enricher,
QObject *parent)
: CompletionEngine(parent)
, m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_contextManager(contextManager)
, m_performanceLogger(performanceLogger)
, m_enricher(enricher)
{
}
FimCompletionEngine::~FimCompletionEngine()
{
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.constBegin(); it != requests.constEnd(); ++it) {
if (it.value().provider)
it.value().provider->cancelRequest(it.key());
}
}
void FimCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
const QString error = QString("Document is not available: %1").arg(context.filePath);
LOG_MESSAGE("Error: " + error);
emit completionFailed(requestId, error);
return;
}
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
auto updatedContext = reader.prepareContext(context.line, context.column, m_completeSettings);
bool isPreset1Active = m_contextManager.isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
const QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
const QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == Templates::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == Providers::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager.openedFiles({context.filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager.openedFilesContext({context.filePath}));
}
}
if (m_enricher && promptTemplate->type() == Templates::TemplateType::Chat
&& m_completeSettings.completionMode.stringValue()
== QLatin1StringView(Constants::CC_MODE_FIM_WITH_CONTEXT)) {
const QString enrichment = m_enricher->enrichmentFor(
documentInfo,
updatedContext.prefix.value_or(""),
context.line,
context.column,
kSemanticContextTokenBudget);
if (!enrichment.isEmpty())
systemPrompt.append(enrichment);
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == Templates::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&FimCompletionEngine::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&FimCompletionEngine::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&FimCompletionEngine::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto llmRequestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests.insert(llmRequestId, {requestId, context.filePath, provider});
m_performanceLogger.startTimeMeasurement(llmRequestId);
}
void FimCompletionEngine::cancel(quint64 requestId)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().requestId != requestId)
continue;
const auto llmRequestId = it.key();
auto *provider = it.value().provider;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(llmRequestId);
if (provider)
provider->cancelRequest(llmRequestId);
return;
}
}
void FimCompletionEngine::handleFullResponse(
const ::LLMQore::RequestID &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
emit completionReady(active.requestId, postProcess(fullText, active.filePath));
}
void FimCompletionEngine::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void FimCompletionEngine::handleRequestFailed(
const ::LLMQore::RequestID &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
QString FimCompletionEngine::resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isPreset1Active) const
{
const QString custom = isPreset1Active ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
QString FimCompletionEngine::postProcess(const QString &completion, const QString &filePath) const
{
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
const QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
QString processedCompletion;
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion, filePath);
} else {
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion, filePath)
: completion;
}
if (processedCompletion.endsWith('\n')) {
QString withoutTrailing = processedCompletion.chopped(1);
if (!withoutTrailing.contains('\n')) {
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
processedCompletion = withoutTrailing;
}
}
LOG_MESSAGE(QString("Completion after filter: \n%1").arg(processedCompletion));
return processedCompletion;
}
} // namespace QodeAssist

View File

@@ -0,0 +1,69 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QHash>
#include "completion/CompletionEngine.hpp"
#include "context/CompletionContextEnricher.hpp"
#include "context/ContextManager.hpp"
#include "context/IDocumentReader.hpp"
#include "logger/IRequestPerformanceLogger.hpp"
#include "providers/IProviderRegistry.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "templates/IPromptProvider.hpp"
namespace QodeAssist {
class FimCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
FimCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
Context::ContextManager &contextManager,
IRequestPerformanceLogger &performanceLogger,
Context::ICompletionEnricher *enricher = nullptr,
QObject *parent = nullptr);
~FimCompletionEngine() override;
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
private slots:
void handleFullResponse(const ::LLMQore::RequestID &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const ::LLMQore::RequestID &requestId, const QString &error);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
Providers::Provider *provider = nullptr;
};
QString resolveEndpoint(Templates::PromptTemplate *promptTemplate, bool isPreset1Active) const;
QString postProcess(const QString &completion, const QString &filePath) const;
const Settings::GeneralSettings &m_generalSettings;
const Settings::CodeCompletionSettings &m_completeSettings;
Providers::IProviderRegistry &m_providerRegistry;
Templates::IPromptProvider *m_promptProvider = nullptr;
Context::IDocumentReader &m_documentReader;
Context::ContextManager &m_contextManager;
IRequestPerformanceLogger &m_performanceLogger;
Context::ICompletionEnricher *m_enricher = nullptr;
QHash<::LLMQore::RequestID, ActiveRequest> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -1,455 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "completion/CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
{
}
LLMClientInterface::~LLMClientInterface()
{
handleCancelRequest();
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "QodeAssist";
}
void LLMClientInterface::startImpl()
{
emit started();
}
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
sendCompletionToClient(fullText, ctx.originalRequest, true);
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::sendData(const QByteArray &data)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
if (!doc.isObject())
return;
QJsonObject request = doc.object();
QString method = request["method"].toString();
if (method == "initialize") {
handleInitialize(request);
} else if (method == "initialized") {
// TODO make initilizied handler
} else if (method == "shutdown") {
handleShutdown(request);
} else if (method == "textDocument/didOpen") {
handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") {
handleCompletion(request);
} else if (method == "$/cancelRequest") {
handleCancelRequest();
} else if (method == "exit") {
// TODO make exit handler
} else {
LOG_MESSAGE(QString("Unknown method: %1").arg(method));
}
}
void LLMClientInterface::handleCancelRequest()
{
QSet<Providers::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void LLMClientInterface::handleInitialize(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
QJsonObject result;
QJsonObject capabilities;
capabilities["textDocumentSync"] = 1;
capabilities["completionProvider"] = QJsonObject{{"resolveProvider", false}};
capabilities["hoverProvider"] = true;
result["capabilities"] = capabilities;
QJsonObject serverInfo;
serverInfo["name"] = "QodeAssist LSP Server";
serverInfo["version"] = "0.1";
result["serverInfo"] = serverInfo;
response["result"] = result;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleShutdown(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
response["result"] = QJsonValue();
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
void LLMClientInterface::handleInitialized(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["method"] = "initialized";
response["params"] = QJsonObject();
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleExit(const QJsonObject &request)
{
emit finished();
}
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = errorMessage;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
if (!documentInfo.document) {
QString error = QString("Document is not available: %1").arg(filePath);
LOG_MESSAGE("Error: " + error);
sendErrorResponse(request, error);
return;
}
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == Templates::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == Providers::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == Templates::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId);
}
LLMCore::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject();
int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
}
QString LLMClientInterface::resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
{
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
Context::ContextManager *LLMClientInterface::contextManager() const
{
return m_contextManager;
}
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result;
QJsonArray completions;
QJsonObject completionItem;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
QString processedCompletion;
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion,
Context::extractFilePathFromRequest(request));
} else { // "Auto"
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion,
Context::extractFilePathFromRequest(
request))
: completion;
}
if (processedCompletion.endsWith('\n')) {
QString withoutTrailing = processedCompletion.chopped(1);
if (!withoutTrailing.contains('\n')) {
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
processedCompletion = withoutTrailing;
}
}
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range;
range["start"] = position;
range["end"] = position;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);
result["completions"] = completions;
result[LanguageServerProtocol::isIncompleteKey] = !isComplete;
response[LanguageServerProtocol::resultKey] = result;
LOG_MESSAGE(
QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE(
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
} // namespace QodeAssist

View File

@@ -1,93 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <LLMQore/BaseClient.hpp>
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include "llmcore/ContextData.hpp"
#include "templates/IPromptProvider.hpp"
#include "providers/IProviderRegistry.hpp"
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply;
class QNetworkAccessManager;
namespace QodeAssist {
class LLMClientInterface : public LanguageClient::BaseClientInterface
{
Q_OBJECT
public:
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete);
void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected:
void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request);
void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request);
void handleExit(const QJsonObject &request);
void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
struct RequestContext
{
QJsonObject originalRequest;
Providers::Provider *provider;
};
LLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
Templates::IPromptProvider *m_promptProvider = nullptr;
Providers::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -1,147 +0,0 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* 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/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/
#pragma once
#include <languageserverprotocol/jsonkeys.h>
#include <languageserverprotocol/jsonrpcmessages.h>
#include <languageserverprotocol/lsptypes.h>
namespace QodeAssist {
class Completion : public LanguageServerProtocol::JsonObject
{
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
public:
using JsonObject::JsonObject;
QString displayText() const { return typedValue<QString>(displayTextKey); }
LanguageServerProtocol::Position position() const
{
return typedValue<LanguageServerProtocol::Position>(LanguageServerProtocol::positionKey);
}
void setRange(const LanguageServerProtocol::Range &range)
{
insert(LanguageServerProtocol::rangeKey, range);
}
LanguageServerProtocol::Range range() const
{
return typedValue<LanguageServerProtocol::Range>(LanguageServerProtocol::rangeKey);
}
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
QString uuid() const { return typedValue<QString>(uuidKey); }
bool isValid() const override
{
return contains(LanguageServerProtocol::textKey)
&& contains(LanguageServerProtocol::rangeKey)
&& contains(LanguageServerProtocol::positionKey);
}
};
class GetCompletionParams : public LanguageServerProtocol::JsonObject
{
public:
static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams(
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version,
const LanguageServerProtocol::Position &position)
{
setTextDocument(document);
setVersion(version);
setPosition(position);
}
using JsonObject::JsonObject;
// The text document.
LanguageServerProtocol::TextDocumentIdentifier textDocument() const
{
return typedValue<LanguageServerProtocol::TextDocumentIdentifier>(docKey);
}
void setTextDocument(const LanguageServerProtocol::TextDocumentIdentifier &id)
{
insert(docKey, id);
}
// The position inside the text document.
LanguageServerProtocol::Position position() const
{
return LanguageServerProtocol::fromJsonValue<LanguageServerProtocol::Position>(
value(docKey).toObject().value(LanguageServerProtocol::positionKey));
}
void setPosition(const LanguageServerProtocol::Position &position)
{
QJsonObject result = value(docKey).toObject();
result[LanguageServerProtocol::positionKey] = (QJsonObject) position;
insert(docKey, result);
}
// The version
int version() const { return typedValue<int>(LanguageServerProtocol::versionKey); }
void setVersion(int version)
{
QJsonObject result = value(docKey).toObject();
result[LanguageServerProtocol::versionKey] = version;
insert(docKey, result);
}
bool isValid() const override
{
return contains(docKey)
&& value(docKey).toObject().contains(LanguageServerProtocol::positionKey)
&& value(docKey).toObject().contains(LanguageServerProtocol::versionKey);
}
};
class GetCompletionResponse : public LanguageServerProtocol::JsonObject
{
static constexpr LanguageServerProtocol::Key completionKey{"completions"};
public:
using JsonObject::JsonObject;
LanguageServerProtocol::LanguageClientArray<Completion> completions() const
{
return clientArray<Completion>(completionKey);
}
};
class GetCompletionRequest
: public LanguageServerProtocol::Request<GetCompletionResponse, std::nullptr_t, GetCompletionParams>
{
public:
explicit GetCompletionRequest(const GetCompletionParams &params = {})
: Request(methodName, params)
{}
using Request::Request;
constexpr static const LanguageServerProtocol::Key methodName{"getCompletionsCycling"};
};
} // namespace QodeAssist

View File

@@ -2,6 +2,7 @@ add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
FileEditManager.hpp FileEditManager.cpp
ContextManager.hpp ContextManager.cpp
CompletionContextEnricher.hpp CompletionContextEnricher.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
@@ -21,6 +22,9 @@ target_link_libraries(Context
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
QtCreator::CPlusPlus
QtCreator::CppEditor
QtCreator::QmlJS
QodeAssistLogger
QodeAssistSettings
)

View File

@@ -0,0 +1,282 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionContextEnricher.hpp"
#include <QRegularExpression>
#include <QSet>
#include <cplusplus/Literals.h>
#include <cplusplus/Names.h>
#include <cplusplus/Overview.h>
#include <cplusplus/Scope.h>
#include <cplusplus/Symbols.h>
#include <cppeditor/cppmodelmanager.h>
#include <qmljs/qmljsdocument.h>
#include <qmljs/qmljsmodelmanagerinterface.h>
#include <utils/filepath.h>
#include "ProgrammingLanguage.hpp"
#include "TokenUtils.hpp"
namespace QodeAssist::Context {
namespace {
constexpr int kMaxPrefixIdentifiers = 40;
constexpr int kMaxEnclosingMembers = 40;
constexpr int kMaxReferencedDeclarations = 60;
constexpr int kMaxIncludedDocuments = 30;
constexpr int kMaxSiblingComponents = 60;
constexpr int kMaxVisitedSymbols = 5000;
constexpr int kPrefixScanWindow = 4096;
const QSet<QString> &cppKeywords()
{
static const QSet<QString> keywords = {
"alignas", "alignof", "asm", "auto", "bool", "break",
"case", "catch", "char", "class", "const", "constexpr",
"consteval", "constinit","continue", "decltype", "default", "delete",
"do", "double", "else", "enum", "explicit", "export",
"extern", "false", "float", "for", "friend", "goto",
"if", "inline", "int", "long", "mutable", "namespace",
"new", "noexcept", "nullptr", "operator", "private", "protected",
"public", "register", "requires", "return", "short", "signed",
"sizeof", "static", "struct", "switch", "template", "this",
"throw", "true", "try", "typedef", "typeid", "typename",
"union", "unsigned", "using", "virtual", "void", "volatile",
"while", "override", "final", "co_await", "co_return", "co_yield"};
return keywords;
}
QString prettySymbol(const CPlusPlus::Overview &overview, CPlusPlus::Symbol *symbol)
{
return overview.prettyType(symbol->type(), overview.prettyName(symbol->name()));
}
QString cppEnclosingScopeSection(
const CPlusPlus::Document::Ptr &document,
const CPlusPlus::Overview &overview,
int line,
int column)
{
CPlusPlus::Scope *scope = document->scopeAt(line + 1, column + 1);
for (CPlusPlus::Scope *current = scope; current; current = current->enclosingScope()) {
CPlusPlus::Class *klass = current->asClass();
if (!klass || !klass->name())
continue;
QStringList members;
const int memberCount = qMin(klass->memberCount(), kMaxEnclosingMembers);
for (int i = 0; i < memberCount; ++i) {
CPlusPlus::Symbol *member = klass->memberAt(i);
if (!member || !member->name())
continue;
members.append(" " + prettySymbol(overview, member));
}
if (members.isEmpty())
continue;
return QString("Enclosing class %1:\n%2")
.arg(overview.prettyName(klass->name()), members.join('\n'));
}
return {};
}
QString cppReferencedDeclarationsSection(
const CPlusPlus::Snapshot &snapshot,
const CPlusPlus::Document::Ptr &document,
const CPlusPlus::Overview &overview,
const QString &prefix)
{
const QStringList identifiers
= identifiersNearCursor(prefix.right(kPrefixScanWindow), kMaxPrefixIdentifiers);
if (identifiers.isEmpty())
return {};
QSet<QByteArray> wanted;
for (const QString &identifier : identifiers)
wanted.insert(identifier.toUtf8());
QList<CPlusPlus::Document::Ptr> documents{document};
const Utils::FilePaths includes = document->includedFiles();
for (const Utils::FilePath &include : includes) {
if (documents.size() > kMaxIncludedDocuments)
break;
if (auto included = snapshot.document(include))
documents.append(included);
}
QStringList declarations;
QSet<QString> seen;
int visited = 0;
auto collect = [&](auto self, CPlusPlus::Scope *scope) -> void {
if (!scope || declarations.size() >= kMaxReferencedDeclarations
|| visited >= kMaxVisitedSymbols)
return;
for (int i = 0; i < scope->memberCount(); ++i) {
if (declarations.size() >= kMaxReferencedDeclarations
|| ++visited >= kMaxVisitedSymbols)
return;
CPlusPlus::Symbol *symbol = scope->memberAt(i);
if (!symbol || !symbol->name())
continue;
if (const CPlusPlus::Identifier *nameId = symbol->name()->asNameId()) {
const QByteArray key
= QByteArray::fromRawData(nameId->chars(), int(nameId->size()));
if (wanted.contains(key)) {
const QString pretty = " " + prettySymbol(overview, symbol);
if (!seen.contains(pretty)) {
seen.insert(pretty);
declarations.append(pretty);
}
}
}
if (symbol->asFunction() || symbol->asBlock())
continue;
if (auto nested = symbol->asScope())
self(self, nested);
}
};
for (const auto &doc : std::as_const(documents)) {
if (doc->globalNamespace())
collect(collect, doc->globalNamespace());
}
if (declarations.isEmpty())
return {};
return QString("Declarations referenced near the cursor:\n%1").arg(declarations.join('\n'));
}
QStringList cppEnrichment(const QString &filePath, const QString &prefix, int line, int column)
{
auto *modelManager = CppEditor::CppModelManager::instance();
if (!modelManager)
return {};
const CPlusPlus::Snapshot snapshot = modelManager->snapshot();
const CPlusPlus::Document::Ptr document
= snapshot.document(Utils::FilePath::fromString(filePath));
if (!document || !document->globalNamespace())
return {};
CPlusPlus::Overview overview;
overview.showReturnTypes = true;
overview.showArgumentNames = true;
QStringList sections;
const QString enclosing = cppEnclosingScopeSection(document, overview, line, column);
if (!enclosing.isEmpty())
sections.append(enclosing);
const QString referenced
= cppReferencedDeclarationsSection(snapshot, document, overview, prefix);
if (!referenced.isEmpty())
sections.append(referenced);
return sections;
}
QStringList qmlEnrichment(const QString &filePath)
{
auto *modelManager = QmlJS::ModelManagerInterface::instance();
if (!modelManager)
return {};
const QmlJS::Snapshot snapshot = modelManager->snapshot();
const Utils::FilePath path = Utils::FilePath::fromString(filePath);
const QmlJS::Document::Ptr document = snapshot.document(path);
const Utils::FilePath directory = document ? document->path() : path.parentDir();
QStringList components;
const QList<QmlJS::Document::Ptr> siblings = snapshot.documentsInDirectory(directory);
for (const auto &sibling : siblings) {
if (components.size() >= kMaxSiblingComponents)
break;
if (!sibling || sibling->fileName() == path)
continue;
const QString component = sibling->componentName();
if (!component.isEmpty() && !components.contains(component))
components.append(component);
}
if (components.isEmpty())
return {};
components.sort();
return QStringList{
QString("QML components available in this directory: %1").arg(components.join(", "))};
}
} // anonymous namespace
QString SemanticContextEnricher::enrichmentFor(
const DocumentInfo &documentInfo, const QString &prefix, int line, int column, int maxTokens)
{
const ProgrammingLanguage language
= ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType);
QStringList sections;
switch (language) {
case ProgrammingLanguage::Cpp:
sections = cppEnrichment(documentInfo.filePath, prefix, line, column);
break;
case ProgrammingLanguage::QML:
sections = qmlEnrichment(documentInfo.filePath);
break;
default:
return {};
}
const QString clamped = clampSectionsToTokenBudget(sections, maxTokens);
if (clamped.isEmpty())
return {};
return QString("\n\n# Project code model context (may lag unsaved edits)\n%1\n").arg(clamped);
}
QString clampSectionsToTokenBudget(const QStringList &sections, int maxTokens)
{
QStringList kept;
int remaining = maxTokens;
for (const QString &section : sections) {
if (section.isEmpty())
continue;
const int cost = TokenUtils::estimateTokens(section);
if (cost > remaining)
continue;
kept.append(section);
remaining -= cost;
}
return kept.join("\n\n");
}
QStringList identifiersNearCursor(const QString &prefix, int maxIdentifiers)
{
static const QRegularExpression identifierRegex(
QStringLiteral("[A-Za-z_][A-Za-z0-9_]{2,}"));
QStringList result;
QSet<QString> seen;
QRegularExpressionMatchIterator it = identifierRegex.globalMatch(prefix);
QStringList all;
while (it.hasNext())
all.append(it.next().captured());
for (auto identifier = all.crbegin(); identifier != all.crend(); ++identifier) {
if (result.size() >= maxIdentifiers)
break;
if (seen.contains(*identifier) || cppKeywords().contains(*identifier))
continue;
seen.insert(*identifier);
result.append(*identifier);
}
return result;
}
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,42 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
#include <QStringList>
#include "IDocumentReader.hpp"
namespace QodeAssist::Context {
class ICompletionEnricher
{
public:
virtual ~ICompletionEnricher() = default;
virtual QString enrichmentFor(
const DocumentInfo &documentInfo,
const QString &prefix,
int line,
int column,
int maxTokens)
= 0;
};
class SemanticContextEnricher : public ICompletionEnricher
{
public:
QString enrichmentFor(
const DocumentInfo &documentInfo,
const QString &prefix,
int line,
int column,
int maxTokens) override;
};
QString clampSectionsToTokenBudget(const QStringList &sections, int maxTokens);
QStringList identifiersNearCursor(const QString &prefix, int maxIdentifiers);
} // namespace QodeAssist::Context

View File

@@ -4,11 +4,11 @@
#include "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo>
#include <QTextBlock>
#include "CodeCompletionSettings.hpp"
#include "ProgrammingLanguage.hpp"
const QRegularExpression &getYearRegex()
{
@@ -108,7 +108,8 @@ QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosi
QString DocumentContextReader::getLanguageAndFileInfo() const
{
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
QString language
= ProgrammingLanguageUtils::toString(ProgrammingLanguageUtils::fromMimeType(m_mimeType));
QString fileExtension = QFileInfo(m_filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")

View File

@@ -0,0 +1,109 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CompletionMcpServer.hpp"
#include <QHostAddress>
#include <QUuid>
#include <LLMQore/BaseTool.hpp>
#include <LLMQore/McpHttpServerTransport.hpp>
#include <LLMQore/McpServer.hpp>
#include <LLMQore/McpTypes.hpp>
#include <LLMQore/Version.hpp>
#include <logger/Logger.hpp>
namespace QodeAssist::Mcp {
CompletionMcpServer::CompletionMcpServer(QObject *parent)
: QObject(parent)
{}
CompletionMcpServer::~CompletionMcpServer()
{
stop();
}
QString CompletionMcpServer::serverName()
{
return QStringLiteral("qodeassist-completion");
}
QString CompletionMcpServer::endpointUrl() const
{
return m_runningPort == 0
? QString()
: QStringLiteral("http://127.0.0.1:%1/mcp/%2").arg(m_runningPort).arg(m_pathToken);
}
QString CompletionMcpServer::start(::LLMQore::BaseTool *proposeTool)
{
if (!proposeTool)
return {};
if (m_runningPort != 0)
return endpointUrl();
m_pathToken = QUuid::createUuid().toString(QUuid::WithoutBraces);
::LLMQore::Mcp::HttpServerConfig transportConfig;
transportConfig.address = QHostAddress::LocalHost;
transportConfig.port = 0;
transportConfig.path = QStringLiteral("/mcp/%1").arg(m_pathToken);
m_transport = new ::LLMQore::Mcp::McpHttpServerTransport(transportConfig, this);
::LLMQore::Mcp::McpServerConfig serverConfig;
serverConfig.serverInfo = {"QodeAssist Completion", QStringLiteral(LLMQORE_VERSION_STRING)};
serverConfig.instructions = tr(
"Inline code-completion channel. Call propose_completion exactly once with the completion "
"text for the requested cursor position; it is shown to the user as a ghost-text "
"suggestion. Do not edit files.");
m_server = new ::LLMQore::Mcp::McpServer(m_transport.data(), serverConfig, this);
m_server->addTool(proposeTool);
connect(
m_server,
&::LLMQore::Mcp::McpServer::toolCallReceived,
this,
[](const QString &toolName) {
LOG_MESSAGE(
QString("Completion MCP server: incoming tools/call for '%1'").arg(toolName));
});
m_server->start();
if (!m_transport->isOpen()) {
LOG_MESSAGE("The QodeAssist completion MCP server could not bind a loopback port");
stop();
return {};
}
m_runningPort = m_transport->serverPort();
LOG_MESSAGE(
QString("QodeAssist completion MCP server listening on %1").arg(endpointUrl()));
return endpointUrl();
}
void CompletionMcpServer::stop()
{
if (m_server) {
m_server->stop();
m_server->deleteLater();
m_server.clear();
}
if (m_transport) {
m_transport->deleteLater();
m_transport.clear();
}
m_runningPort = 0;
m_pathToken.clear();
}
} // namespace QodeAssist::Mcp

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QPointer>
#include <QString>
namespace LLMQore {
class BaseTool;
namespace Mcp {
class McpServer;
class McpHttpServerTransport;
} // namespace Mcp
} // namespace LLMQore
namespace QodeAssist::Mcp {
class CompletionMcpServer : public QObject
{
Q_OBJECT
public:
explicit CompletionMcpServer(QObject *parent = nullptr);
~CompletionMcpServer() override;
QString start(::LLMQore::BaseTool *proposeTool);
void stop();
static QString serverName();
QString endpointUrl() const;
quint16 runningPort() const { return m_runningPort; }
private:
QPointer<::LLMQore::Mcp::McpServer> m_server;
QPointer<::LLMQore::Mcp::McpHttpServerTransport> m_transport;
QString m_pathToken;
quint16 m_runningPort = 0;
};
} // namespace QodeAssist::Mcp

View File

@@ -23,7 +23,6 @@
#include <coreplugin/navigationwidget.h>
#include <coreplugin/statusbarmanager.h>
#include <extensionsystem/iplugin.h>
#include <languageclient/languageclientmanager.h>
#include <texteditor/texteditor.h>
#include <utils/icon.h>
@@ -35,7 +34,13 @@
#include <QInputDialog>
#include "ConfigurationManager.hpp"
#include "completion/QodeAssistClient.hpp"
#include "completion/AcpCompletionEngine.hpp"
#include "completion/AgenticCompletionEngine.hpp"
#include "completion/CodeCompletionController.hpp"
#include "completion/FimCompletionEngine.hpp"
#include "context/CompletionContextEnricher.hpp"
#include "context/ContextManager.hpp"
#include "tools/ProposeCompletionTool.hpp"
#include "UpdateStatusWidget.hpp"
#include "plugin/Version.hpp"
#include "chat/ChatEditor.hpp"
@@ -63,6 +68,7 @@
#include <QQmlContext>
#include <ChatView/ChatView.hpp>
#include <acp/AgentCatalog.hpp>
#include <acp/AgentCatalogStore.hpp>
#include <settings/AgentsWidget.hpp>
#include <ChatView/AttachmentStaging.hpp>
@@ -78,7 +84,28 @@
#include <texteditor/texteditorconstants.h>
#ifdef WITH_TESTS
#include "QodeAssistTest.hpp"
#include "AcpChatBackendTest.hpp"
#include "AcpCompletionEngineTest.hpp"
#include "AgentBindingTest.hpp"
#include "AgentCatalogTest.hpp"
#include "AgentKnowledgeServerTest.hpp"
#include "AgenticCompletionEngineTest.hpp"
#include "BlockCodecTest.hpp"
#include "ChatFileStoreTest.hpp"
#include "ChatHistorySerializerTest.hpp"
#include "ChatViewTest.hpp"
#include "ClaudeCacheControlTest.hpp"
#include "CodeHandlerTest.hpp"
#include "ConversationCoordinatorTest.hpp"
#include "DocumentContextReaderTest.hpp"
#include "FimCompletionEngineTest.hpp"
#include "LlmChatBackendTest.hpp"
#include "LlmSuggestionTest.hpp"
#include "RowAudienceTest.hpp"
#include "SessionPermissionsTest.hpp"
#include "SessionTest.hpp"
#include "ToolsManagerGateTest.hpp"
#include "TurnContextTest.hpp"
#endif
using namespace Utils;
@@ -101,8 +128,12 @@ public:
~QodeAssistPlugin() final
{
Chat::AttachmentStaging::cleanupGlobalIntermediateStorage();
delete m_qodeAssistClient;
delete m_completionController;
delete m_fimEngine;
delete m_agenticEngine;
delete m_acpEngine;
delete m_completionContextManager;
if (m_chatOutputPane) {
delete m_chatOutputPane;
}
@@ -161,8 +192,8 @@ public:
requestAction.setDefaultKeySequence(defaultShortcut);
requestAction.addOnTriggered(this, [this] {
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
m_qodeAssistClient->requestCompletions(editor);
if (m_completionController) {
m_completionController->requestCompletions(editor);
} else
qWarning() << "The QodeAssist is not ready. Please check your connection and "
"settings.";
@@ -209,6 +240,8 @@ public:
Settings::setupProjectPanel();
ConfigurationManager::instance().init();
m_proposeCompletionTool = new Tools::ProposeCompletionTool(this);
m_mcpServerManager = new Mcp::McpServerManager(this);
m_mcpServerManager->init();
@@ -226,7 +259,7 @@ public:
quickRefactorAction.setIcon(QCODEASSIST_ICON.icon());
quickRefactorAction.addOnTriggered(this, [this] {
if (auto editor = TextEditor::TextEditorWidget::currentTextEditorWidget()) {
if (m_qodeAssistClient && m_qodeAssistClient->reachable()) {
if (m_completionController) {
QuickRefactorDialog
dialog(Core::ICore::dialogParent(), m_lastRefactorInstructions);
@@ -234,7 +267,7 @@ public:
QString instructions = dialog.instructions();
if (!instructions.isEmpty()) {
m_lastRefactorInstructions = instructions;
m_qodeAssistClient->requestQuickRefactor(editor, instructions);
m_completionController->requestQuickRefactor(editor, instructions);
}
}
} else {
@@ -319,36 +352,84 @@ public:
Chat::AttachmentStaging::cleanupGlobalIntermediateStorage();
#ifdef WITH_TESTS
addTest<QodeAssistTest>();
addTest<CodeHandlerTest>();
addTest<LlmSuggestionTest>();
addTest<ClaudeCacheControlTest>();
addTest<DocumentContextReaderTest>();
addTest<ChatHistorySerializerTest>();
addTest<SessionTest>();
addTest<RowAudienceTest>();
addTest<SessionPermissionsTest>();
addTest<TurnContextTest>();
addTest<AgentCatalogTest>();
addTest<AcpChatBackendTest>();
addTest<ToolsManagerGateTest>();
addTest<AgentKnowledgeServerTest>();
addTest<AgentBindingTest>();
addTest<LlmChatBackendTest>();
addTest<ConversationCoordinatorTest>();
addTest<BlockCodecTest>();
addTest<ChatFileStoreTest>();
addTest<ChatViewTest>();
addTest<FimCompletionEngineTest>();
addTest<AgenticCompletionEngineTest>();
addTest<AcpCompletionEngineTest>();
#endif
}
void extensionsInitialized() final {}
void restartClient()
void restartCompletion()
{
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface(
delete m_completionController;
delete m_fimEngine;
delete m_agenticEngine;
delete m_acpEngine;
delete m_completionContextManager;
m_completionContextManager = new Context::ContextManager(this);
m_fimEngine = new FimCompletionEngine(
Settings::generalSettings(),
Settings::codeCompletionSettings(),
Providers::ProvidersManager::instance(),
&m_promptProvider,
m_documentReader,
m_performanceLogger));
*m_completionContextManager,
m_performanceLogger,
&m_completionEnricher,
this);
m_agenticEngine = new AgenticCompletionEngine(
Settings::generalSettings(),
Settings::codeCompletionSettings(),
Providers::ProvidersManager::instance(),
&m_promptProvider,
m_documentReader,
m_performanceLogger,
this);
m_acpEngine = new AcpCompletionEngine(
Settings::codeCompletionSettings(),
Settings::generalSettings(),
[this](const QString &id) -> std::optional<Acp::AgentDefinition> {
if (!m_agentCatalog)
return std::nullopt;
return m_agentCatalog->catalog().agent(id);
},
m_documentReader,
m_proposeCompletionTool,
this);
m_completionController = new CodeCompletionController(
m_fimEngine, m_agenticEngine, m_acpEngine, *m_completionContextManager, this);
}
bool delayedInitialize() final
{
restartClient();
restartCompletion();
return true;
}
ShutdownFlag aboutToShutdown() final
{
if (!m_qodeAssistClient)
return SynchronousShutdown;
connect(m_qodeAssistClient, &QObject::destroyed, this, &IPlugin::asynchronousShutdownFinished);
return AsynchronousShutdown;
return SynchronousShutdown;
}
private:
@@ -491,7 +572,13 @@ private:
m_statusWidget->showUpdateAvailable(info.version);
}
QPointer<QodeAssistClient> m_qodeAssistClient;
QPointer<CodeCompletionController> m_completionController;
QPointer<FimCompletionEngine> m_fimEngine;
QPointer<AgenticCompletionEngine> m_agenticEngine;
QPointer<AcpCompletionEngine> m_acpEngine;
QPointer<Tools::ProposeCompletionTool> m_proposeCompletionTool;
QPointer<Context::ContextManager> m_completionContextManager;
Context::SemanticContextEnricher m_completionEnricher;
Templates::PromptProviderFim m_promptProvider;
Context::DocumentReaderQtCreator m_documentReader;
RequestPerformanceLogger m_performanceLogger;

View File

@@ -60,17 +60,47 @@ CodeCompletionSettings::CodeCompletionSettings()
Tr::tr("Hint-based: Shows a hint when typing, press Tab to request completion\n"
"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(
triggerMode.setLabelText(Tr::tr("Trigger mode:"));
triggerMode.setSettingsKey(Constants::CC_TRIGGER_MODE);
triggerMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
triggerMode.addOption("Automatic");
triggerMode.addOption("Manual");
triggerMode.setDefaultValue("Automatic");
triggerMode.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)."));
completionMode.setLabelText(Tr::tr("Completion mode:"));
completionMode.setSettingsKey(Constants::CC_COMPLETION_MODE);
completionMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
completionMode.addOption(Constants::CC_MODE_CLASSIC_FIM);
completionMode.addOption(Constants::CC_MODE_FIM_WITH_CONTEXT);
completionMode.addOption(Constants::CC_MODE_AGENTIC_LOCAL);
completionMode.addOption(Constants::CC_MODE_AGENTIC_ACP);
completionMode.setDefaultValue(Constants::CC_MODE_CLASSIC_FIM);
completionMode.setToolTip(
Tr::tr("How a completion is produced.\n"
"Classic FIM: one fill-in-the-middle request to the configured provider.\n"
"FIM + context: the same request enriched with semantic context from the IDE "
"code model (C++/QML, chat-type templates only; the built-in model may lag "
"unsaved edits).\n"
"Agentic (local tools): a tool-capable model runs the in-process tool loop, "
"gathers its own context and proposes the completion via the "
"propose_completion tool (needs a tool-capable provider and a chat-type "
"template; multi-round and slow, so it is invoked only by the manual "
"'Request QodeAssist Suggestion' shortcut, never auto-triggered while typing).\n"
"Agentic (ACP agent): an external ACP agent (chosen below) calls "
"propose_completion over the QodeAssist MCP server; manual trigger only. "
"With no agent chosen the feature is disabled."));
completionAgentId.setSettingsKey(Constants::CC_ACP_AGENT_ID);
completionAgentId.setLabelText(Tr::tr("ACP completion agent id:"));
completionAgentId.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
completionAgentId.setToolTip(
Tr::tr("The id of the ACP agent (from the Agents catalogue) used for the "
"'Agentic (ACP agent)' completion mode. Leave empty to keep that mode disabled."));
smartContextTrigger.setSettingsKey(Constants::CC_SMART_CONTEXT_TRIGGER);
smartContextTrigger.setLabelText(Tr::tr("Smart context-aware triggering"));
smartContextTrigger.setDefaultValue(true);
@@ -125,37 +155,6 @@ CodeCompletionSettings::CodeCompletionSettings()
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("Tab");
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)"));
ignoreWhitespaceInCharCount.setSettingsKey(Constants::CC_IGNORE_WHITESPACE_IN_CHAR_COUNT);
ignoreWhitespaceInCharCount.setLabelText(
Tr::tr("Ignore spaces and tabs in character count"));
@@ -387,7 +386,9 @@ CodeCompletionSettings::CodeCompletionSettings()
autoCompletion,
multiLineCompletion,
Row{modelOutputHandler, Stretch{1}},
Row{triggerMode, Stretch{1}},
Row{completionMode, Stretch{1}},
Row{completionAgentId, Stretch{1}},
showProgressWidget,
useOpenFilesContext,
respectQtcPopup,
@@ -488,13 +489,12 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(useOpenFilesContext);
resetAspect(modelOutputHandler);
resetAspect(completionTriggerMode);
resetAspect(triggerMode);
resetAspect(completionMode);
resetAspect(completionAgentId);
resetAspect(smartContextTrigger);
resetAspect(respectQtcPopup);
resetAspect(cancelOnInput);
resetAspect(hintCharThreshold);
resetAspect(hintHideTimeout);
resetAspect(hintTriggerKey);
resetAspect(ignoreWhitespaceInCharCount);
resetAspect(abortAssistOnRequest);
writeSettings();
@@ -504,16 +504,19 @@ void CodeCompletionSettings::resetSettingsToDefaults()
void CodeCompletionSettings::migrateCompletionMode()
{
auto *qtcSettings = Core::ICore::settings();
if (!qtcSettings || qtcSettings->contains(Constants::CC_COMPLETION_MODE))
if (!qtcSettings || !qtcSettings->contains(Constants::CC_COMPLETION_TRIGGER_MODE))
return;
const QString oldMode = completionTriggerMode.stringValue();
if (oldMode.startsWith("Hint"))
completionMode.setStringValue("Manual");
else
completionMode.setStringValue("Automatic");
if (!qtcSettings->contains(Constants::CC_TRIGGER_MODE)) {
const QString oldMode = completionTriggerMode.stringValue();
if (oldMode.startsWith("Hint"))
triggerMode.setStringValue("Manual");
else
triggerMode.setStringValue("Automatic");
}
writeSettings();
qtcSettings->remove(Constants::CC_COMPLETION_TRIGGER_MODE);
}
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const

View File

@@ -22,7 +22,9 @@ public:
Utils::BoolAspect multiLineCompletion{this};
Utils::SelectionAspect modelOutputHandler{this};
Utils::SelectionAspect completionTriggerMode{this};
Utils::SelectionAspect triggerMode{this};
Utils::SelectionAspect completionMode{this};
Utils::StringAspect completionAgentId{this};
Utils::BoolAspect smartContextTrigger{this};
Utils::BoolAspect respectQtcPopup{this};
Utils::BoolAspect cancelOnInput{this};
@@ -30,9 +32,6 @@ public:
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::BoolAspect ignoreWhitespaceInCharCount{this};
Utils::StringListAspect customLanguages{this};

View File

@@ -67,13 +67,16 @@ 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_COMPLETION_MODE[] = "QodeAssist.ccCompletionMode";
const char CC_TRIGGER_MODE[] = "QodeAssist.ccCompletionMode";
const char CC_COMPLETION_MODE[] = "QodeAssist.ccCompletionProduceMode";
const char CC_MODE_CLASSIC_FIM[] = "Classic FIM";
const char CC_MODE_FIM_WITH_CONTEXT[] = "FIM + context";
const char CC_MODE_AGENTIC_LOCAL[] = "Agentic (local tools)";
const char CC_MODE_AGENTIC_ACP[] = "Agentic (ACP agent)";
const char CC_ACP_AGENT_ID[] = "QodeAssist.ccAcpAgentId";
const char CC_SMART_CONTEXT_TRIGGER[] = "QodeAssist.ccSmartContextTrigger";
const char CC_RESPECT_QTC_POPUP[] = "QodeAssist.ccRespectQtcPopup";
const char CC_CANCEL_ON_INPUT[] = "QodeAssist.ccCancelOnInput";
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 CC_IGNORE_WHITESPACE_IN_CHAR_COUNT[] = "QodeAssist.ccIgnoreWhitespaceInCharCount";
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";

View File

@@ -0,0 +1,104 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "tools/ProposeCompletionTool.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <logger/Logger.hpp>
namespace QodeAssist::Tools {
ProposeCompletionTool::ProposeCompletionTool(QObject *parent)
: ::LLMQore::BaseTool(parent)
{
}
QString ProposeCompletionTool::toolId()
{
return QStringLiteral("propose_completion");
}
QString ProposeCompletionTool::id() const
{
return toolId();
}
QString ProposeCompletionTool::displayName() const
{
return tr("Propose Completion");
}
QString ProposeCompletionTool::description() const
{
return tr(
"Propose the code completion text for the current cursor position. Pass the actual "
"code to insert in the `text` argument — it must be the real, non-empty continuation, "
"never an empty string. The text is shown to the user as an inline ghost-text "
"suggestion they accept with Tab. Call this exactly once, after you have worked out "
"the completion; it inserts nothing by itself.");
}
QJsonObject ProposeCompletionTool::parametersSchema() const
{
return QJsonObject{
{"type", "object"},
{"properties",
QJsonObject{
{"file",
QJsonObject{
{"type", "string"},
{"description", "Absolute path of the file the completion is for."}}},
{"line",
QJsonObject{
{"type", "integer"},
{"description", "Zero-based line of the cursor position."}}},
{"column",
QJsonObject{
{"type", "integer"},
{"description", "Zero-based column of the cursor position."}}},
{"text",
QJsonObject{
{"type", "string"},
{"description",
"The actual code to insert at the cursor position, verbatim. Must be the "
"real, non-empty continuation — never an empty string."}}}}},
{"required", QJsonArray{"text"}}};
}
::LLMQore::ToolSafety ProposeCompletionTool::safety() const
{
return ::LLMQore::ToolSafety::ReadOnly;
}
QFuture<::LLMQore::ToolResult> ProposeCompletionTool::executeAsync(const QJsonObject &input)
{
LOG_MESSAGE(
QString("propose_completion invoked with input: %1")
.arg(QString::fromUtf8(QJsonDocument(input).toJson(QJsonDocument::Compact))));
const QString filePath = input.value("file").toString();
const QString text = input.value("text").toString();
if (text.isEmpty()) {
LOG_MESSAGE(QStringLiteral("propose_completion rejected: text is empty"));
return QtFuture::makeReadyValueFuture(
::LLMQore::ToolResult::error(QStringLiteral(
"The 'text' argument was empty. Work out the actual code to insert at the cursor "
"and call propose_completion again with that non-empty completion text.")));
}
const int line = input.value("line").toInt(-1);
const int column = input.value("column").toInt(-1);
emit completionProposed(filePath, line, column, text);
return QtFuture::makeReadyValueFuture(
::LLMQore::ToolResult::text(
QStringLiteral("Completion proposal shown to the user as a suggestion.")));
}
} // namespace QodeAssist::Tools

View File

@@ -0,0 +1,32 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <LLMQore/BaseTool.hpp>
namespace QodeAssist::Tools {
class ProposeCompletionTool : public ::LLMQore::BaseTool
{
Q_OBJECT
public:
explicit ProposeCompletionTool(QObject *parent = nullptr);
static QString toolId();
QString id() const override;
QString displayName() const override;
QString description() const override;
QJsonObject parametersSchema() const override;
::LLMQore::ToolSafety safety() const override;
QFuture<::LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
signals:
void completionProposed(const QString &filePath, int line, int column, const QString &text);
};
} // namespace QodeAssist::Tools

View File

@@ -1,57 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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

@@ -1,34 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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

@@ -1,68 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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

@@ -1,29 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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

@@ -1,125 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "EditorChatButton.hpp"
#include <utils/theme/theme.h>
#include <QMouseEvent>
#include <QPainter>
namespace QodeAssist {
EditorChatButton::EditorChatButton(QWidget *parent)
: QWidget(parent)
{
m_textColor = Utils::creatorTheme()->color(Utils::Theme::TextColorNormal);
m_backgroundColor = Utils::creatorTheme()->color(Utils::Theme::BackgroundColorNormal);
m_logoPixmap = QPixmap(":/resources/images/qoderassist-icon.png");
if (!m_logoPixmap.isNull()) {
QImage image = m_logoPixmap.toImage();
image = image.convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
QColor pixelColor = QColor::fromRgba(image.pixel(x, y));
int brightness = (pixelColor.red() + pixelColor.green() + pixelColor.blue()) / 3;
if (brightness > 200) {
pixelColor.setAlpha(0);
image.setPixelColor(x, y, pixelColor);
} else if (pixelColor.alpha() > 0) {
int alpha = pixelColor.alpha();
pixelColor = m_textColor;
pixelColor.setAlpha(alpha);
image.setPixelColor(x, y, pixelColor);
}
}
}
m_logoPixmap = QPixmap::fromImage(image);
m_logoPixmap = m_logoPixmap.scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
setFixedSize(40, 40);
setCursor(Qt::PointingHandCursor);
setToolTip(tr("Open QodeAssist Chat"));
}
EditorChatButton::~EditorChatButton() = default;
void EditorChatButton::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QColor bgColor = m_backgroundColor;
if (m_isPressed) {
bgColor = bgColor.darker(120);
} else if (m_isHovered) {
bgColor = bgColor.lighter(110);
}
painter.fillRect(rect(), bgColor);
QRect buttonRect = rect().adjusted(4, 4, -4, -4);
painter.setPen(Qt::NoPen);
QColor buttonBgColor
= m_isPressed ? Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover).darker(110)
: Utils::creatorTheme()->color(Utils::Theme::BackgroundColorHover);
if (m_isHovered) {
buttonBgColor = buttonBgColor.lighter(110);
}
painter.setBrush(buttonBgColor);
painter.drawEllipse(buttonRect);
if (!m_logoPixmap.isNull()) {
QRect logoRect(
(width() - m_logoPixmap.width()) / 2,
(height() - m_logoPixmap.height()) / 2,
m_logoPixmap.width(),
m_logoPixmap.height());
painter.drawPixmap(logoRect, m_logoPixmap);
}
}
void EditorChatButton::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_isPressed = true;
update();
}
QWidget::mousePressEvent(event);
}
void EditorChatButton::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_isPressed) {
m_isPressed = false;
update();
if (rect().contains(event->pos())) {
emit clicked();
}
}
QWidget::mouseReleaseEvent(event);
}
void EditorChatButton::enterEvent(QEnterEvent *event)
{
m_isHovered = true;
update();
QWidget::enterEvent(event);
}
void EditorChatButton::leaveEvent(QEvent *event)
{
m_isHovered = false;
m_isPressed = false;
update();
QWidget::leaveEvent(event);
}
} // namespace QodeAssist

View File

@@ -1,38 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QPushButton>
#include <QTimer>
#include <QWidget>
namespace QodeAssist {
class EditorChatButton : public QWidget
{
Q_OBJECT
public:
explicit EditorChatButton(QWidget *parent = nullptr);
~EditorChatButton() override;
signals:
void clicked();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QPixmap m_logoPixmap;
QColor m_textColor;
QColor m_backgroundColor;
bool m_isPressed = false;
bool m_isHovered = false;
};
} // namespace QodeAssist

View File

@@ -1,74 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "EditorChatButtonHandler.hpp"
#include "EditorChatButton.hpp"
#include <texteditor/texteditor.h>
#include <utils/tooltip/tooltip.h>
#include <QPoint>
namespace QodeAssist {
EditorChatButtonHandler::~EditorChatButtonHandler()
{
delete m_chatButton;
}
void EditorChatButtonHandler::showButton(TextEditor::TextEditorWidget *widget)
{
if (!widget)
return;
m_widget = widget;
identifyMatch(widget, widget->textCursor().position(), [this](auto priority) {
if (priority != Priority_None && m_widget) {
const QTextCursor cursor = m_widget->textCursor();
const QRect selectionRect = m_widget->cursorRect(cursor);
m_cursorPosition = m_widget->viewport()->mapToGlobal(selectionRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
operateTooltip(m_widget, m_cursorPosition);
}
});
}
void EditorChatButtonHandler::hideButton()
{
Utils::ToolTip::hide();
}
void EditorChatButtonHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report)
{
if (!editorWidget) {
report(Priority_None);
return;
}
report(Priority_Tooltip);
}
void EditorChatButtonHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget, const QPoint &point)
{
if (!editorWidget)
return;
if (!Utils::ToolTip::isVisible()) {
m_chatButton = new EditorChatButton(editorWidget);
m_buttonHeight = m_chatButton->height();
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::show(showPoint, m_chatButton, editorWidget);
} else {
QPoint showPoint = point;
showPoint.ry() -= m_buttonHeight;
Utils::ToolTip::move(showPoint);
}
}
} // namespace QodeAssist

View File

@@ -1,37 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "widgets/EditorChatButton.hpp"
#include <texteditor/basehoverhandler.h>
#include <QPointer>
namespace QodeAssist {
class EditorChatButtonHandler : public TextEditor::BaseHoverHandler
{
public:
explicit EditorChatButtonHandler() = default;
~EditorChatButtonHandler() override;
void showButton(TextEditor::TextEditorWidget *widget);
void hideButton();
signals:
void chatButtonClicked(TextEditor::TextEditorWidget *widget);
protected:
void identifyMatch(
TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
private:
QPointer<TextEditor::TextEditorWidget> m_widget;
QPoint m_cursorPosition;
EditorChatButton *m_chatButton = nullptr;
int m_buttonHeight = 0;
};
} // namespace QodeAssist