mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-24 20:20:59 -04:00
feat: add support code completion via acp
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user