mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 11:41:03 -04:00
feat: add support code completion via acp
This commit is contained in:
@@ -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
37
docs/completion-modes.md
Normal 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.
|
||||
427
sources/completion/AcpCompletionEngine.cpp
Normal file
427
sources/completion/AcpCompletionEngine.cpp
Normal 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
|
||||
86
sources/completion/AcpCompletionEngine.hpp
Normal file
86
sources/completion/AcpCompletionEngine.hpp
Normal 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
|
||||
249
sources/completion/AgenticCompletionEngine.cpp
Normal file
249
sources/completion/AgenticCompletionEngine.cpp
Normal 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
|
||||
61
sources/completion/AgenticCompletionEngine.hpp
Normal file
61
sources/completion/AgenticCompletionEngine.hpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
34
sources/completion/CompletionEngine.hpp
Normal file
34
sources/completion/CompletionEngine.hpp
Normal 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
|
||||
292
sources/completion/FimCompletionEngine.cpp
Normal file
292
sources/completion/FimCompletionEngine.cpp
Normal 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
|
||||
69
sources/completion/FimCompletionEngine.hpp
Normal file
69
sources/completion/FimCompletionEngine.hpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶ms = {})
|
||||
: Request(methodName, params)
|
||||
{}
|
||||
using Request::Request;
|
||||
constexpr static const LanguageServerProtocol::Key methodName{"getCompletionsCycling"};
|
||||
};
|
||||
} // namespace QodeAssist
|
||||
@@ -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
|
||||
)
|
||||
|
||||
282
sources/context/CompletionContextEnricher.cpp
Normal file
282
sources/context/CompletionContextEnricher.cpp
Normal 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 §ions, int maxTokens)
|
||||
{
|
||||
QStringList kept;
|
||||
int remaining = maxTokens;
|
||||
for (const QString §ion : 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
|
||||
42
sources/context/CompletionContextEnricher.hpp
Normal file
42
sources/context/CompletionContextEnricher.hpp
Normal 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 §ions, int maxTokens);
|
||||
QStringList identifiersNearCursor(const QString &prefix, int maxIdentifiers);
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
@@ -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")
|
||||
|
||||
109
sources/mcp/CompletionMcpServer.cpp
Normal file
109
sources/mcp/CompletionMcpServer.cpp
Normal 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
|
||||
43
sources/mcp/CompletionMcpServer.hpp
Normal file
43
sources/mcp/CompletionMcpServer.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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";
|
||||
|
||||
104
sources/tools/ProposeCompletionTool.cpp
Normal file
104
sources/tools/ProposeCompletionTool.cpp
Normal 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
|
||||
32
sources/tools/ProposeCompletionTool.hpp
Normal file
32
sources/tools/ProposeCompletionTool.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user