Files
dstalk/plugins_base/lsp/src/lsp_plugin.cpp
XiuChengWu c0af9c65c7
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / Sanitizer (ASan+UBSan) / ubuntu-24.04 (push) Has been cancelled
CI / Coverage (gcovr) / ubuntu-24.04 (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
feat: Add LSP plugin unit tests and frontend common initialization library
- Introduced `dstalk_lsp_plugin_test` for testing LSP plugin functionalities including `lsp_trim`, `lsp_frame_message`, and `lsp_parse_content_length`.
- Created `dstalk_frontend_common` static library to encapsulate shared initialization logic for frontend components (CLI, GUI, Web).
- Implemented configuration file discovery and service querying in `dstalk_frontend_init`.
- Added internal headers for LSP and Anthropic plugins to facilitate unit testing.
- Established a mailroom system for asynchronous message passing between stateless agents, enhancing coordination and context management.
2026-06-01 08:51:40 +08:00

873 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @file lsp_plugin.cpp
* @brief LSP plugin: Language Server Protocol JSON-RPC client for diagnostics, hover, completion.
* LSP 插件Language Server Protocol JSON-RPC 客户端,用于诊断、悬停、补全。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
// plugin_lsp — LSP (Language Server Protocol) 服务 / LSP (Language Server Protocol) service
//
// 自行管理语言服务器子进程,使用 JSON-RPC 2.0 over stdio 通信 / Self-manages language server subprocess, communicates via JSON-RPC 2.0 over stdio.
// 无外部服务依赖(不依赖 http/config 等其他插件) / No external service dependencies (does not depend on http/config or other plugins).
#include "dstalk/dstalk_host.h"
#include "dstalk/dstalk_services.h"
#include "lsp_internal.hpp"
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
// ============================================================================
// 平台相关 — 子进程管理 (内嵌 subprocess::Process) / Platform specific — subprocess management (embedded subprocess::Process)
// ============================================================================
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace json = boost::json;
// ============================================================================
// 全局指针 / Global pointers
// ============================================================================
static const dstalk_host_api_t* g_host = nullptr;
// ============================================================================
// 子进程封装 (内嵌 subprocess.hpp) / Subprocess wrapper (embedded subprocess.hpp)
// ============================================================================
struct Process {
#ifdef _WIN32
HANDLE hProcess = INVALID_HANDLE_VALUE;
HANDLE hThread = INVALID_HANDLE_VALUE;
HANDLE hStdIn = INVALID_HANDLE_VALUE;
HANDLE hStdOut = INVALID_HANDLE_VALUE;
#else
pid_t pid = -1;
int stdin_fd = -1;
int stdout_fd = -1;
#endif
// 从给定命令行启动子进程。为 stdin/stdout 设置管道 / Start a child process from the given command line. Sets up pipes for stdin/stdout.
bool start(const char* cmd) {
if (!cmd || !cmd[0]) return false;
stop();
#ifdef _WIN32
SECURITY_ATTRIBUTES sa = {};
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
HANDLE child_stdin_read = INVALID_HANDLE_VALUE;
HANDLE child_stdout_write = INVALID_HANDLE_VALUE;
if (!CreatePipe(&child_stdin_read, &hStdIn, &sa, 0)) goto win32_fail;
if (!SetHandleInformation(hStdIn, HANDLE_FLAG_INHERIT, 0)) goto win32_fail;
if (!CreatePipe(&hStdOut, &child_stdout_write, &sa, 0)) goto win32_fail;
if (!SetHandleInformation(hStdOut, HANDLE_FLAG_INHERIT, 0)) goto win32_fail;
{
STARTUPINFOW si = {};
si.cb = sizeof(STARTUPINFOW);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = child_stdin_read;
si.hStdOutput = child_stdout_write;
si.hStdError = child_stdout_write;
PROCESS_INFORMATION pi = {};
std::string cmd_copy(cmd);
wchar_t wcmd[4096] = {};
if (MultiByteToWideChar(CP_UTF8, 0, cmd_copy.c_str(), -1, wcmd, 4096) == 0)
goto win32_fail;
if (!CreateProcessW(nullptr, wcmd, nullptr, nullptr, TRUE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi))
goto win32_fail;
hProcess = pi.hProcess;
hThread = pi.hThread;
}
CloseHandle(child_stdin_read);
CloseHandle(child_stdout_write);
return true;
win32_fail:
if (child_stdin_read != INVALID_HANDLE_VALUE) CloseHandle(child_stdin_read);
if (child_stdout_write != INVALID_HANDLE_VALUE) CloseHandle(child_stdout_write);
if (hStdIn != INVALID_HANDLE_VALUE) { CloseHandle(hStdIn); hStdIn = INVALID_HANDLE_VALUE; }
if (hStdOut != INVALID_HANDLE_VALUE) { CloseHandle(hStdOut); hStdOut = INVALID_HANDLE_VALUE; }
if (hProcess != INVALID_HANDLE_VALUE) { CloseHandle(hProcess); hProcess = INVALID_HANDLE_VALUE; }
if (hThread != INVALID_HANDLE_VALUE) { CloseHandle(hThread); hThread = INVALID_HANDLE_VALUE; }
return false;
#else
int pin[2] = {-1, -1};
int pout[2] = {-1, -1};
if (pipe(pin) != 0) goto posix_fail;
if (pipe(pout) != 0) goto posix_fail;
pid = fork();
if (pid < 0) goto posix_fail;
if (pid == 0) {
dup2(pin[0], STDIN_FILENO);
close(pin[0]); close(pin[1]);
dup2(pout[1], STDOUT_FILENO);
close(pout[0]); close(pout[1]);
int max_fd = static_cast<int>(sysconf(_SC_OPEN_MAX));
if (max_fd > 3) {
for (int i = 3; i < max_fd; ++i) close(i);
}
char* argv[64] = {};
int argc = 0;
char* cmd_copy = strdup(cmd);
char* token = strtok(cmd_copy, " \t");
while (token && argc < 63) {
argv[argc++] = token;
token = strtok(nullptr, " \t");
}
argv[argc] = nullptr;
execvp(argv[0], argv);
_exit(127);
}
close(pin[0]);
close(pout[1]);
stdin_fd = pin[1];
stdout_fd = pout[0];
return true;
posix_fail:
if (pin[0] != -1) close(pin[0]);
if (pin[1] != -1) close(pin[1]);
if (pout[0] != -1) close(pout[0]);
if (pout[1] != -1) close(pout[1]);
stdin_fd = -1;
stdout_fd = -1;
pid = -1;
return false;
#endif
}
// 优雅终止子进程,回退到 SIGKILL/TerminateProcess / Gracefully terminate the child process, with fallback to SIGKILL/TerminateProcess.
void stop() {
#ifdef _WIN32
if (hProcess != INVALID_HANDLE_VALUE) {
WaitForSingleObject(hProcess, 2000);
TerminateProcess(hProcess, 1);
CloseHandle(hProcess); hProcess = INVALID_HANDLE_VALUE;
}
if (hThread != INVALID_HANDLE_VALUE) { CloseHandle(hThread); hThread = INVALID_HANDLE_VALUE; }
if (hStdIn != INVALID_HANDLE_VALUE) { CloseHandle(hStdIn); hStdIn = INVALID_HANDLE_VALUE; }
if (hStdOut != INVALID_HANDLE_VALUE) { CloseHandle(hStdOut); hStdOut = INVALID_HANDLE_VALUE; }
#else
if (pid > 0) {
kill(pid, SIGTERM);
int status = 0;
for (int i = 0; i < 20; ++i) {
if (waitpid(pid, &status, WNOHANG) > 0) break;
usleep(100000);
}
if (waitpid(pid, &status, WNOHANG) == 0) {
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
}
pid = -1;
}
if (stdin_fd != -1) { close(stdin_fd); stdin_fd = -1; }
if (stdout_fd != -1) { close(stdout_fd); stdout_fd = -1; }
#endif
}
// 将数据字符串写入子进程 stdin 管道 / Write a data string to the child's stdin pipe.
bool write(const std::string& data) {
if (data.empty()) return true;
#ifdef _WIN32
if (hStdIn == INVALID_HANDLE_VALUE) return false;
DWORD written = 0;
return WriteFile(hStdIn, data.c_str(), static_cast<DWORD>(data.size()), &written, nullptr)
&& written == data.size();
#else
if (stdin_fd < 0) return false;
size_t total = 0;
const char* buf = data.c_str();
size_t len = data.size();
while (total < len) {
ssize_t n = ::write(stdin_fd, buf + total, len - total);
if (n <= 0) return false;
total += static_cast<size_t>(n);
}
return true;
#endif
}
// 从子进程 stdout 管道读取一行(到并包括 '\n' / Read one line (up to and including '\n') from the child's stdout pipe.
bool read_line(std::string& line) {
line.clear();
#ifdef _WIN32
if (hStdOut == INVALID_HANDLE_VALUE) return false;
char ch; DWORD nread = 0;
while (true) {
if (!ReadFile(hStdOut, &ch, 1, &nread, nullptr)) return false;
if (nread == 0) return false;
line += ch;
if (ch == '\n') return true;
}
#else
if (stdout_fd < 0) return false;
char ch;
while (true) {
ssize_t n = ::read(stdout_fd, &ch, 1);
if (n <= 0) return false;
line += ch;
if (ch == '\n') return true;
}
#endif
}
// 从子进程 stdout 管道读取恰好 count 字节到 buf / Read exactly `count` bytes from the child's stdout pipe into `buf`.
bool read_bytes(std::string& buf, int count) {
if (count <= 0) { buf.clear(); return true; }
#ifdef _WIN32
if (hStdOut == INVALID_HANDLE_VALUE) return false;
buf.resize(static_cast<size_t>(count) + 1);
DWORD total = 0, nread = 0;
while (total < static_cast<DWORD>(count)) {
if (!ReadFile(hStdOut, const_cast<char*>(buf.data()) + total,
static_cast<DWORD>(count) - total, &nread, nullptr))
return false;
if (nread == 0) return false;
total += nread;
}
buf[count] = '\0';
buf.resize(count);
return true;
#else
if (stdout_fd < 0) return false;
buf.resize(count);
size_t total = 0;
while (total < static_cast<size_t>(count)) {
ssize_t n = ::read(stdout_fd, const_cast<char*>(buf.data()) + total,
static_cast<size_t>(count) - total);
if (n <= 0) return false;
total += static_cast<size_t>(n);
}
return true;
#endif
}
};
// ============================================================================
// LSP 状态(静态单例) / LSP state (static singleton)
// ============================================================================
struct LspState {
Process proc;
std::atomic<bool> running{false};
std::string language;
std::atomic<int> next_id{1};
// 响应用于同步等待 / Responses for synchronous waiting
std::mutex mutex;
std::condition_variable cv;
std::unordered_map<int, std::string> pending_responses;
// 诊断缓存: URI -> JSON 字符串 / Diagnostics cache: URI -> JSON string
std::unordered_map<std::string, std::string> diagnostics;
// 读取线程 / Reader thread
std::thread reader_thread;
};
static LspState g_lsp;
// ============================================================================
// 辅助函数 / Helper functions
// ============================================================================
// 去除 string_view 首尾空白 / Trim leading and trailing whitespace from a string_view.
std::string_view lsp_trim(std::string_view sv) {
while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t' ||
sv.front() == '\r' || sv.front() == '\n'))
sv.remove_prefix(1);
while (!sv.empty() && (sv.back() == ' ' || sv.back() == '\t' ||
sv.back() == '\r' || sv.back() == '\n'))
sv.remove_suffix(1);
return sv;
}
// 将 JSON-RPC 消息体包装在 LSP 头中 (Content-Length: ...\r\n\r\n) / Wrap a JSON-RPC message body in an LSP header (Content-Length: ...\r\n\r\n).
std::string lsp_frame_message(const std::string& body) {
std::string frame;
frame.reserve(64 + body.size());
frame += "Content-Length: ";
frame += std::to_string(body.size());
frame += "\r\n\r\n";
frame += body;
return frame;
}
// 从 LSP 头行中解析 Content-Length 值。解析失败返回 -1 / Parse the Content-Length value from an LSP header line. Returns -1 on parse failure.
int lsp_parse_content_length(const std::string& line) {
auto sv = lsp_trim(std::string_view(line));
const char prefix[] = "Content-Length:";
const size_t prefix_len = sizeof(prefix) - 1;
if (sv.size() <= prefix_len) return -1;
for (size_t i = 0; i < prefix_len; ++i) {
if (std::tolower(static_cast<unsigned char>(sv[i])) !=
std::tolower(static_cast<unsigned char>(prefix[i])))
return -1;
}
std::string_view num_sv = sv.substr(prefix_len);
while (!num_sv.empty() && (num_sv.front() == ' ' || num_sv.front() == '\t'))
num_sv.remove_prefix(1);
try { return std::stoi(std::string(num_sv)); }
catch (...) { return -1; }
}
// ============================================================================
// JSON-RPC 消息发送 / JSON-RPC message sending
// ============================================================================
// 向 LSP 服务器发送 JSON-RPC 请求并返回分配的请求 id / Send a JSON-RPC request to the LSP server and return the assigned request id.
static int send_request(const std::string& method, const json::object& params) {
int id = g_lsp.next_id.fetch_add(1);
json::object msg;
msg["jsonrpc"] = "2.0";
msg["id"] = id;
msg["method"] = method;
msg["params"] = params;
std::string body = json::serialize(msg);
g_lsp.proc.write(lsp_frame_message(body));
return id;
}
// 向 LSP 服务器发送 JSON-RPC 通知(无 id 字段,不期待响应) / Send a JSON-RPC notification to the LSP server (no id field, no response expected).
static void send_notification(const std::string& method, const json::object& params) {
json::object msg;
msg["jsonrpc"] = "2.0";
msg["method"] = method;
msg["params"] = params;
std::string body = json::serialize(msg);
g_lsp.proc.write(lsp_frame_message(body));
}
// ============================================================================
// 消息处理 / Message handling
// ============================================================================
// 分发接收到的 JSON-RPC 消息:将响应路由到待处理队列,
// 处理 textDocument/publishDiagnostics 通知并存入诊断缓存 / Dispatch a received JSON-RPC message: route responses to pending queue,
// handle textDocument/publishDiagnostics notifications into diagnostics cache.
static void handle_message(const std::string& body) {
try {
json::value val;
try { val = json::parse(body); }
catch (...) { return; }
json::object msg;
try { msg = val.as_object(); }
catch (...) { return; }
if (msg.contains("id") && !msg.contains("method")) {
// 响应 (有 id, 无 method) / Response (has id, no method)
int id = static_cast<int>(msg["id"].as_int64());
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.pending_responses[id] = body;
g_lsp.cv.notify_all();
} else if (msg.contains("method") && !msg.contains("id")) {
// 通知 (有 method, 无 id) / Notification (has method, no id)
std::string method;
try { method = json::value_to<std::string>(msg["method"]); }
catch (...) { return; }
if (method == "textDocument/publishDiagnostics") {
if (!msg.contains("params")) return;
auto params = msg["params"].as_object();
if (!params.contains("uri")) return;
std::string uri = json::value_to<std::string>(params["uri"]);
std::string diag_json;
if (params.contains("diagnostics"))
diag_json = json::serialize(params["diagnostics"]);
else
diag_json = "[]";
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.diagnostics[uri] = diag_json;
}
}
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: unknown exception");
}
}
// ============================================================================
// 读取线程主循环 / Reader thread main loop
// ============================================================================
// 读取线程主循环:解析 LSP header+body 帧并分发消息 / Main loop for the reader thread: parse LSP header+body frames and dispatch messages.
static void reader_loop() {
try {
while (g_lsp.running) {
int content_length = -1;
bool pipe_ok = true;
// 状态机式读取 header 块:循环 read_line 直到读到空行 / State-machine header block read: loop read_line until empty line
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header / LSP 3.17: header block ends with empty line (\r\n), allows other headers like Content-Type
while (pipe_ok) {
std::string line;
if (!g_lsp.proc.read_line(line)) {
pipe_ok = false;
break;
}
// header 块以空行结束 / header block ends with empty line
auto sv = lsp_trim(std::string_view(line));
if (sv.empty()) break;
// 累积 Content-Length遇到其他 header 不丢弃,继续读取下一行 / Accumulate Content-Length; don't discard other headers, continue reading next line
int len = lsp_parse_content_length(line);
if (len >= 0) content_length = len;
}
if (!pipe_ok) break;
// 空行前都没读到 Content-Length协议错误——记日志并跳过这一帧 / Content-Length not read before empty line, protocol error — log and skip this frame
if (content_length < 0) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header");
continue;
}
std::string body;
if (!g_lsp.proc.read_bytes(body, content_length)) break;
handle_message(body);
}
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: unknown exception");
}
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.running = false;
g_lsp.cv.notify_all();
}
// ============================================================================
// LSP 服务 vtable 实现 (定义在 vtable 变量之前) / LSP service vtable implementation (defined before vtable variable)
// ============================================================================
static void g_lsp_impl_stop();
static void g_lsp_impl_stop_nolock();
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock);
// 启动 LSP 服务器进程,发送 initialize/initialized 握手,启动读取线程 / Start the LSP server process, send initialize/initialized handshake, start reader thread.
static int g_lsp_impl_start(const char* server_cmd, const char* language) {
if (!server_cmd || !server_cmd[0]) return -1;
try {
// 如果已在运行, 先停止 / If already running, stop first
if (g_lsp.running) {
g_lsp_impl_stop();
}
g_lsp.language = language ? language : "";
// 启动进程 / Start process
if (!g_lsp.proc.start(server_cmd)) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd);
return -1;
}
// 重置 ID 计数器 / Reset ID counter
g_lsp.next_id = 1;
// 启动读取线程 / Start reader thread
g_lsp.running = true;
g_lsp.reader_thread = std::thread(reader_loop);
// 构建 initialize 参数 / Build initialize params
json::object text_doc_caps;
{
json::object hover;
hover["dynamicRegistration"] = false;
text_doc_caps["hover"] = hover;
json::object completion;
completion["dynamicRegistration"] = false;
text_doc_caps["completion"] = completion;
json::object diagnostic;
diagnostic["dynamicRegistration"] = false;
text_doc_caps["diagnostic"] = diagnostic;
}
json::object capabilities;
capabilities["textDocument"] = text_doc_caps;
json::object init_params;
init_params["processId"] = nullptr;
init_params["rootUri"] = nullptr;
init_params["capabilities"] = capabilities;
// 发送 initialize 请求 / Send initialize request
int init_id = send_request("initialize", init_params);
// 等待 initialize 响应 (最多 10 秒) / Wait for initialize response (max 10 seconds)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(init_id) > 0;
});
if (!got || !g_lsp.running) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] initialize timed out");
g_lsp_impl_stop_locked(lock);
return -1;
}
g_lsp.pending_responses.erase(init_id);
}
// 发送 initialized 通知 / Send initialized notification
send_notification("initialized", json::object{});
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: unknown exception");
return -1;
}
}
// 停止 LSP 服务器:发送 shutdown 请求,发送 exit 通知,停止进程和线程 / Stop the LSP server: send shutdown request, send exit notification, stop process & thread.
static void g_lsp_impl_stop_nolock() {
try {
if (!g_lsp.running) return;
// 发送 shutdown 请求 / Send shutdown request
int shutdown_id = send_request("shutdown", json::object{});
// 等待 shutdown 响应 (最多 2 秒) / Wait for shutdown response (max 2 seconds)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(shutdown_id) > 0;
});
g_lsp.pending_responses.clear();
}
// 发送 exit 通知 / Send exit notification
send_notification("exit", json::object{});
// 停止读取线程 / Stop reader thread
g_lsp.running = false;
g_lsp.proc.stop();
if (g_lsp.reader_thread.joinable())
g_lsp.reader_thread.join();
g_lsp.diagnostics.clear();
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server stopped");
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: unknown exception");
}
}
// 公开 stop无锁获取委托给 g_lsp_impl_stop_nolock / Public stop: acquires no lock (delegates to g_lsp_impl_stop_nolock).
static void g_lsp_impl_stop() {
g_lsp_impl_stop_nolock();
}
// Stop 辅助函数:在调用 g_lsp_impl_stop_nolock 前解锁给定的 unique_lock / Stop helper: unlocks the given unique_lock before calling g_lsp_impl_stop_nolock.
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock) {
lock.unlock();
g_lsp_impl_stop_nolock();
}
// 向 LSP 服务器发送 textDocument/didOpen 通知 / Send a textDocument/didOpen notification to the LSP server.
static int g_lsp_impl_open_document(const char* uri, const char* content,
const char* lang_id) {
if (!g_lsp.running) return -1;
if (!uri || !content || !lang_id) return -1;
try {
json::object text_doc;
text_doc["uri"] = uri;
text_doc["languageId"] = lang_id;
text_doc["version"] = 1;
text_doc["text"] = content;
json::object params;
params["textDocument"] = text_doc;
send_notification("textDocument/didOpen", params);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: unknown exception");
return -1;
}
}
// 向 LSP 服务器发送 textDocument/didClose 通知 / Send a textDocument/didClose notification to the LSP server.
static int g_lsp_impl_close_document(const char* uri) {
if (!g_lsp.running) return -1;
if (!uri) return -1;
try {
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
send_notification("textDocument/didClose", params);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: unknown exception");
return -1;
}
}
// 返回给定文档 URI 的缓存诊断 JSON / Return the cached diagnostics JSON for the given document URI.
static int g_lsp_impl_get_diagnostics(const char* uri, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
try {
std::lock_guard<std::mutex> lock(g_lsp.mutex);
auto it = g_lsp.diagnostics.find(uri);
if (it == g_lsp.diagnostics.end()) {
*json_out = g_host->strdup("[]");
} else {
*json_out = g_host->strdup(it->second.c_str());
}
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: unknown exception");
*json_out = nullptr;
return -1;
}
}
// 发送 textDocument/hover 请求并以 JSON 返回悬停结果 / Send a textDocument/hover request and return the hover result as JSON.
static int g_lsp_impl_get_hover(const char* uri, int line, int col, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
try {
json::object position;
position["line"] = line;
position["character"] = col;
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
int req_id = send_request("textDocument/hover", params);
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: unknown exception");
*json_out = nullptr;
return -1;
}
}
// 发送 textDocument/completion 请求并以 JSON 返回补全列表 / Send a textDocument/completion request and return the completion list as JSON.
static int g_lsp_impl_get_completion(const char* uri, int line, int col, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
try {
json::object position;
position["line"] = line;
position["character"] = col;
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
int req_id = send_request("textDocument/completion", params);
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: unknown exception");
*json_out = nullptr;
return -1;
}
}
// ============================================================================
// 服务 vtable / Service vtable
// ============================================================================
static dstalk_lsp_service_t g_service_vtable = {
&g_lsp_impl_start,
&g_lsp_impl_stop,
&g_lsp_impl_open_document,
&g_lsp_impl_close_document,
&g_lsp_impl_get_diagnostics,
&g_lsp_impl_get_hover,
&g_lsp_impl_get_completion,
};
// ============================================================================
// 生命周期回调 / Lifecycle callbacks
// ============================================================================
// 插件初始化:保存主机指针并注册 lsp 服务 / Plugin init: store host pointer and register the lsp service.
static int on_init(const dstalk_host_api_t* host) {
g_host = host;
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] initializing LSP service plugin");
return host->register_service("lsp", 1, &g_service_vtable);
}
// 插件关闭:如果运行中则停止 LSP 服务器,清空主机指针 / Plugin shutdown: stop LSP server if running, null out host pointer.
static void on_shutdown() {
try {
if (g_lsp.running) {
g_lsp_impl_stop();
}
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] shutdown");
g_host = nullptr;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: %s", e.what());
g_host = nullptr;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: unknown exception");
g_host = nullptr;
}
}
// ============================================================================
// 插件描述符 / Plugin descriptor
// ============================================================================
static dstalk_plugin_info_t g_info = {
/* .name = */ "lsp",
/* .version = */ "1.0.0",
/* .description = */ "Language Server Protocol client (subprocess manager) / Language Server Protocol 客户端(子进程管理器)",
/* .api_version = */ DSTALK_API_VERSION,
/* .dependencies = */ { NULL }, // 无依赖,自行管理子进程 / No dependencies, self-manages subprocess
/* .on_init = */ on_init,
/* .on_shutdown = */ on_shutdown,
/* .on_event = */ nullptr,
};
// 必须入口点:返回插件描述符给主机 / Mandatory entry point: returns the plugin descriptor to the host.
extern "C" DSTALK_PLUGIN_EXPORT dstalk_plugin_info_t* dstalk_plugin_init(void) {
return &g_info;
}