Add SQL, Erlang, and Forth highlighter implementations and tests for LSP process and transport handling.
- Added highlighters for new languages (SQL, Erlang, Forth) with filetype recognition. - Updated and reorganized syntax files to maintain consistency and modularity. - Introduced LSP transport framing unit tests and JSON decoding/dispatch tests. - Refactored `LspManager`, integrating UTF-16/UTF-8 position conversions and robust diagnostics handling. - Enhanced server start/restart logic with workspace root detection and logging to improve LSP usability.
This commit is contained in:
@@ -1,19 +1,147 @@
|
||||
/*
|
||||
* JsonRpcTransport.cc - placeholder
|
||||
* JsonRpcTransport.cc - minimal stdio JSON-RPC framing (Content-Length)
|
||||
*/
|
||||
#include "JsonRpcTransport.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace kte::lsp {
|
||||
void
|
||||
JsonRpcTransport::send(const std::string &/*method*/, const std::string &/*payload*/)
|
||||
JsonRpcTransport::connect(int inFd, int outFd)
|
||||
{
|
||||
// stub: no-op
|
||||
inFd_ = inFd;
|
||||
outFd_ = outFd;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
JsonRpcTransport::send(const std::string &/*method*/, const std::string &payload)
|
||||
{
|
||||
if (outFd_ < 0)
|
||||
return;
|
||||
const std::string header = "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n";
|
||||
std::lock_guard<std::mutex> lk(writeMutex_);
|
||||
// write header
|
||||
const char *hbuf = header.data();
|
||||
size_t hleft = header.size();
|
||||
while (hleft > 0) {
|
||||
ssize_t n = ::write(outFd_, hbuf, hleft);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
hbuf += static_cast<size_t>(n);
|
||||
hleft -= static_cast<size_t>(n);
|
||||
}
|
||||
// write payload
|
||||
const char *pbuf = payload.data();
|
||||
size_t pleft = payload.size();
|
||||
while (pleft > 0) {
|
||||
ssize_t n = ::write(outFd_, pbuf, pleft);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
pbuf += static_cast<size_t>(n);
|
||||
pleft -= static_cast<size_t>(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
readLineCrlf(int fd, std::string &out, size_t maxLen)
|
||||
{
|
||||
out.clear();
|
||||
char ch;
|
||||
while (true) {
|
||||
ssize_t n = ::read(fd, &ch, 1);
|
||||
if (n == 0)
|
||||
return false; // EOF
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
out.push_back(ch);
|
||||
// Handle CRLF or bare LF as end-of-line
|
||||
if ((out.size() >= 2 && out[out.size() - 2] == '\r' && out[out.size() - 1] == '\n') ||
|
||||
(out.size() >= 1 && out[out.size() - 1] == '\n')) {
|
||||
return true;
|
||||
}
|
||||
if (out.size() > maxLen) {
|
||||
// sanity cap
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::optional<JsonRpcMessage>
|
||||
JsonRpcTransport::read()
|
||||
{
|
||||
return std::nullopt; // stub
|
||||
if (inFd_ < 0)
|
||||
return std::nullopt;
|
||||
// Parse headers (case-insensitive), accept/ignore extras
|
||||
size_t contentLength = 0;
|
||||
while (true) {
|
||||
std::string line;
|
||||
if (!readLineCrlf(inFd_, line, kMaxHeaderLine))
|
||||
return std::nullopt;
|
||||
// Normalize end-of-line handling: consider blank line as end of headers
|
||||
if (line == "\r\n" || line == "\n" || line == "\r")
|
||||
break;
|
||||
// Trim trailing CRLF
|
||||
if (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
|
||||
line.pop_back();
|
||||
}
|
||||
// Find colon
|
||||
auto pos = line.find(':');
|
||||
if (pos == std::string::npos)
|
||||
continue;
|
||||
std::string name = line.substr(0, pos);
|
||||
std::string value = line.substr(pos + 1);
|
||||
// trim leading spaces in value
|
||||
size_t i = 0;
|
||||
while (i < value.size() && (value[i] == ' ' || value[i] == '\t'))
|
||||
++i;
|
||||
value.erase(0, i);
|
||||
// lower-case name for comparison
|
||||
for (auto &c: name)
|
||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
if (name == "content-length") {
|
||||
size_t len = static_cast<size_t>(std::strtoull(value.c_str(), nullptr, 10));
|
||||
if (len > kMaxBody) {
|
||||
return std::nullopt; // drop too-large message
|
||||
}
|
||||
contentLength = len;
|
||||
}
|
||||
// else: ignore other headers
|
||||
}
|
||||
if (contentLength == 0)
|
||||
return std::nullopt;
|
||||
std::string body;
|
||||
body.resize(contentLength);
|
||||
size_t readTotal = 0;
|
||||
while (readTotal < contentLength) {
|
||||
ssize_t n = ::read(inFd_, &body[readTotal], contentLength - readTotal);
|
||||
if (n == 0)
|
||||
return std::nullopt;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return std::nullopt;
|
||||
}
|
||||
readTotal += static_cast<size_t>(n);
|
||||
}
|
||||
return JsonRpcMessage{std::move(body)};
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* JsonRpcTransport.h - placeholder transport for JSON-RPC over stdio (stub)
|
||||
* JsonRpcTransport.h - minimal JSON-RPC over stdio transport
|
||||
*/
|
||||
#ifndef KTE_JSON_RPC_TRANSPORT_H
|
||||
#define KTE_JSON_RPC_TRANSPORT_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
namespace kte::lsp {
|
||||
struct JsonRpcMessage {
|
||||
@@ -18,11 +19,24 @@ public:
|
||||
|
||||
~JsonRpcTransport() = default;
|
||||
|
||||
// Send a method call (request or notification) - stub does nothing
|
||||
// Connect this transport to file descriptors (read from inFd, write to outFd)
|
||||
void connect(int inFd, int outFd);
|
||||
|
||||
// Send a method call (request or notification)
|
||||
// 'payload' should be a complete JSON object string to send as the message body.
|
||||
void send(const std::string &method, const std::string &payload);
|
||||
|
||||
// Blocking read next message (stub => returns nullopt)
|
||||
// Blocking read next message; returns nullopt on EOF or error
|
||||
std::optional<JsonRpcMessage> read();
|
||||
|
||||
private:
|
||||
int inFd_ = -1;
|
||||
int outFd_ = -1;
|
||||
std::mutex writeMutex_;
|
||||
|
||||
// Limits to keep the transport resilient
|
||||
static constexpr size_t kMaxHeaderLine = 16 * 1024; // 16 KiB per header line
|
||||
static constexpr size_t kMaxBody = 64ull * 1024ull * 1024ull; // 64 MiB body cap
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "LspTypes.h"
|
||||
#include "Diagnostic.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
// Callback types (stubs for future phases)
|
||||
@@ -55,6 +56,18 @@ public:
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
virtual std::string getServerName() const = 0;
|
||||
|
||||
// Handlers (optional; set by manager)
|
||||
using DiagnosticsHandler = std::function<void(const std::string & uri,
|
||||
const std::vector<Diagnostic> &diagnostics
|
||||
)
|
||||
>;
|
||||
|
||||
|
||||
virtual void setDiagnosticsHandler(DiagnosticsHandler h)
|
||||
{
|
||||
(void) h;
|
||||
}
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
|
||||
@@ -7,16 +7,42 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <cstdarg>
|
||||
|
||||
#include "../Buffer.h"
|
||||
#include "../Editor.h"
|
||||
#include "BufferChangeTracker.h"
|
||||
#include "LspProcessClient.h"
|
||||
#include "UtfCodec.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace kte::lsp {
|
||||
static void
|
||||
lsp_debug_file(const char *fmt, ...)
|
||||
{
|
||||
FILE *f = std::fopen("/tmp/kte-lsp.log", "a");
|
||||
if (!f)
|
||||
return;
|
||||
// prepend timestamp
|
||||
std::time_t t = std::time(nullptr);
|
||||
char ts[32];
|
||||
std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
std::fprintf(f, "[%s] ", ts);
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::vfprintf(f, fmt, ap);
|
||||
va_end(ap);
|
||||
std::fputc('\n', f);
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
|
||||
LspManager::LspManager(Editor *editor, DiagnosticDisplay *display)
|
||||
: editor_(editor), display_(display)
|
||||
{
|
||||
@@ -53,14 +79,50 @@ LspManager::startServerForBuffer(Buffer *buffer)
|
||||
if (!cfg.autostart) {
|
||||
return false;
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(cfg.command, cfg.args);
|
||||
// Determine root as parent of file for now; future: walk rootPatterns
|
||||
// Allow env override of server path
|
||||
std::string command = cfg.command;
|
||||
if (lang == "cpp") {
|
||||
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||
command = p;
|
||||
} else if (lang == "go") {
|
||||
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||
command = p;
|
||||
} else if (lang == "rust") {
|
||||
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||
command = p;
|
||||
}
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] startServerForBuffer: lang=%s cmd=%s args=%zu file=%s\n",
|
||||
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||
lsp_debug_file("startServerForBuffer: lang=%s cmd=%s args=%zu file=%s",
|
||||
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||
// Wire diagnostics handler to manager
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
// Determine workspace root using rootPatterns if set; fallback to file's parent
|
||||
std::string rootPath;
|
||||
if (!buffer->Filename().empty()) {
|
||||
fs::path p(buffer->Filename());
|
||||
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
|
||||
rootPath = detectWorkspaceRoot(buffer->Filename(), cfg);
|
||||
if (rootPath.empty()) {
|
||||
fs::path p(buffer->Filename());
|
||||
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
|
||||
}
|
||||
}
|
||||
if (debug_) {
|
||||
const char *pathEnv = std::getenv("PATH");
|
||||
std::fprintf(stderr, "[kte][lsp] initializing server: rootPath=%s PATH=%s\n",
|
||||
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||
lsp_debug_file("initializing server: rootPath=%s PATH=%s",
|
||||
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||
}
|
||||
if (!client->initialize(rootPath)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", lang.c_str());
|
||||
lsp_debug_file("initialize failed for lang=%s", lang.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
servers_[lang] = std::move(client);
|
||||
@@ -89,11 +151,84 @@ LspManager::stopAllServers()
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::startServerForLanguage(const std::string &languageId, const std::string &rootPath)
|
||||
{
|
||||
auto cfgIt = serverConfigs_.find(languageId);
|
||||
if (cfgIt == serverConfigs_.end())
|
||||
return false;
|
||||
|
||||
// If already running, nothing to do
|
||||
auto it = servers_.find(languageId);
|
||||
if (it != servers_.end() && it->second && it->second->isRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto &cfg = cfgIt->second;
|
||||
std::string command = cfg.command;
|
||||
if (languageId == "cpp") {
|
||||
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||
command = p;
|
||||
} else if (languageId == "go") {
|
||||
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||
command = p;
|
||||
} else if (languageId == "rust") {
|
||||
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||
command = p;
|
||||
}
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] startServerForLanguage: lang=%s cmd=%s args=%zu root=%s\n",
|
||||
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||
lsp_debug_file("startServerForLanguage: lang=%s cmd=%s args=%zu root=%s",
|
||||
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
std::string root = rootPath;
|
||||
if (!root.empty()) {
|
||||
// keep
|
||||
} else {
|
||||
// Try cwd if not provided
|
||||
root = std::string();
|
||||
}
|
||||
if (!client->initialize(root)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", languageId.c_str());
|
||||
lsp_debug_file("initialize failed for lang=%s", languageId.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
servers_[languageId] = std::move(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::restartServer(const std::string &languageId, const std::string &rootPath)
|
||||
{
|
||||
stopServer(languageId);
|
||||
return startServerForLanguage(languageId, rootPath);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferOpened(Buffer *buffer)
|
||||
{
|
||||
if (!startServerForBuffer(buffer))
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] onBufferOpened: file=%s lang=%s\n",
|
||||
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||
lsp_debug_file("onBufferOpened: file=%s lang=%s",
|
||||
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||
}
|
||||
if (!startServerForBuffer(buffer)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] onBufferOpened: server did not start\n");
|
||||
lsp_debug_file("onBufferOpened: server did not start");
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||
if (!client)
|
||||
return;
|
||||
@@ -102,6 +237,12 @@ LspManager::onBufferOpened(Buffer *buffer)
|
||||
const auto lang = getLanguageId(buffer);
|
||||
const int version = static_cast<int>(buffer->Version());
|
||||
const std::string text = buffer->FullText();
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] didOpen: uri=%s lang=%s version=%d bytes=%zu\n",
|
||||
uri.c_str(), lang.c_str(), version, text.size());
|
||||
lsp_debug_file("didOpen: uri=%s lang=%s version=%d bytes=%zu",
|
||||
uri.c_str(), lang.c_str(), version, text.size());
|
||||
}
|
||||
client->didOpen(uri, lang, version, text);
|
||||
}
|
||||
|
||||
@@ -127,7 +268,38 @@ LspManager::onBufferChanged(Buffer *buffer)
|
||||
ev.text = buffer->FullText();
|
||||
changes.push_back(std::move(ev));
|
||||
}
|
||||
client->didChange(uri, version, changes);
|
||||
|
||||
// Option A: convert ranges from UTF-8 (editor coords) -> UTF-16 (LSP wire)
|
||||
std::vector<TextDocumentContentChangeEvent> changes16;
|
||||
changes16.reserve(changes.size());
|
||||
// LineProvider that serves lines from this buffer by URI
|
||||
Buffer *bufForUri = buffer; // changes are for this buffer
|
||||
auto provider = [bufForUri](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!bufForUri)
|
||||
return std::string_view();
|
||||
const auto &rows = bufForUri->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
// Materialize one line into a thread_local scratch; return view
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
for (const auto &ch: changes) {
|
||||
TextDocumentContentChangeEvent out = ch;
|
||||
if (ch.range.has_value()) {
|
||||
Range r16 = toUtf16(uri, *ch.range, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("didChange range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||
ch.range->start.line, ch.range->start.character,
|
||||
ch.range->end.character,
|
||||
r16.start.line, r16.start.character, r16.end.character);
|
||||
}
|
||||
out.range = r16;
|
||||
}
|
||||
changes16.push_back(std::move(out));
|
||||
}
|
||||
client->didChange(uri, version, changes16);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +329,24 @@ void
|
||||
LspManager::requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
client->completion(getUri(buffer), pos, std::move(callback));
|
||||
const auto uri = getUri(buffer);
|
||||
// Convert position to UTF-16 using Option A provider
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("completion pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
client->completion(uri, p16, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +355,23 @@ void
|
||||
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
client->hover(getUri(buffer), pos, std::move(callback));
|
||||
const auto uri = getUri(buffer);
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("hover pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
client->hover(uri, p16, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +380,23 @@ void
|
||||
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
client->definition(getUri(buffer), pos, std::move(callback));
|
||||
const auto uri = getUri(buffer);
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("definition pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
client->definition(uri, p16, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,14 +404,72 @@ LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback cal
|
||||
void
|
||||
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
|
||||
{
|
||||
diagnosticStore_.setDiagnostics(uri, diagnostics);
|
||||
// Convert incoming ranges from UTF-16 (wire) -> UTF-8 (editor)
|
||||
std::vector<Diagnostic> conv = diagnostics;
|
||||
Buffer *buf = findBufferByUri(uri);
|
||||
auto provider = [buf](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buf)
|
||||
return std::string_view();
|
||||
const auto &rows = buf->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
for (auto &d: conv) {
|
||||
Range r8 = toUtf8(uri, d.range, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("diagnostic range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||
d.range.start.line, d.range.start.character, d.range.end.character,
|
||||
r8.start.line, r8.start.character, r8.end.character);
|
||||
}
|
||||
d.range = r8;
|
||||
}
|
||||
diagnosticStore_.setDiagnostics(uri, conv);
|
||||
if (display_) {
|
||||
display_->updateDiagnostics(uri, diagnostics);
|
||||
display_->updateDiagnostics(uri, conv);
|
||||
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::toggleAutostart(const std::string &languageId)
|
||||
{
|
||||
auto it = serverConfigs_.find(languageId);
|
||||
if (it == serverConfigs_.end())
|
||||
return false;
|
||||
it->second.autostart = !it->second.autostart;
|
||||
return it->second.autostart;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
LspManager::configuredLanguages() const
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(serverConfigs_.size());
|
||||
for (const auto &kv: serverConfigs_)
|
||||
out.push_back(kv.first);
|
||||
std::sort(out.begin(), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
LspManager::runningLanguages() const
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
for (const auto &kv: servers_) {
|
||||
if (kv.second && kv.second->isRunning())
|
||||
out.push_back(kv.first);
|
||||
}
|
||||
std::sort(out.begin(), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::getLanguageId(Buffer *buffer)
|
||||
{
|
||||
@@ -223,6 +502,22 @@ LspManager::getUri(Buffer *buffer)
|
||||
}
|
||||
|
||||
|
||||
// Resolve a Buffer* by matching constructed file URI
|
||||
Buffer *
|
||||
LspManager::findBufferByUri(const std::string &uri)
|
||||
{
|
||||
if (!editor_)
|
||||
return nullptr;
|
||||
// Compare against getUri for each buffer
|
||||
auto &bufs = editor_->Buffers();
|
||||
for (auto &b: bufs) {
|
||||
if (getUri(&b) == uri)
|
||||
return &b;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::extToLanguageId(const std::string &ext)
|
||||
{
|
||||
@@ -268,6 +563,10 @@ LspManager::ensureServerForLanguage(const std::string &languageId)
|
||||
if (cfg == serverConfigs_.end())
|
||||
return nullptr;
|
||||
auto client = std::make_unique<LspProcessClient>(cfg->second.command, cfg->second.args);
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
// No specific file context here; initialize with empty or current working dir
|
||||
if (!client->initialize(""))
|
||||
return nullptr;
|
||||
auto *ret = client.get();
|
||||
@@ -323,4 +622,73 @@ LspManager::patternToLanguageId(const std::string &pattern)
|
||||
return {};
|
||||
return extToLanguageId(ext);
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
|
||||
|
||||
// Detect workspace root by walking up from filePath looking for any of the
|
||||
// configured rootPatterns (simple filenames). Supports comma/semicolon-separated
|
||||
// patterns in cfg.rootPatterns.
|
||||
std::string
|
||||
LspManager::detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg)
|
||||
{
|
||||
if (filePath.empty())
|
||||
return {};
|
||||
fs::path start(filePath);
|
||||
fs::path dir = start.has_parent_path() ? start.parent_path() : start;
|
||||
|
||||
// Build cache key
|
||||
const std::string cacheKey = (dir.string() + "|" + cfg.rootPatterns);
|
||||
auto it = rootCache_.find(cacheKey);
|
||||
if (it != rootCache_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Split patterns by ',', ';', or ':'
|
||||
std::vector<std::string> pats;
|
||||
{
|
||||
std::string acc;
|
||||
for (char c: cfg.rootPatterns) {
|
||||
if (c == ',' || c == ';' || c == ':') {
|
||||
if (!acc.empty()) {
|
||||
pats.push_back(acc);
|
||||
acc.clear();
|
||||
}
|
||||
} else if (!std::isspace(static_cast<unsigned char>(c))) {
|
||||
acc.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!acc.empty())
|
||||
pats.push_back(acc);
|
||||
}
|
||||
// If no patterns defined, cache empty and return {}
|
||||
if (pats.empty()) {
|
||||
rootCache_[cacheKey] = {};
|
||||
return {};
|
||||
}
|
||||
|
||||
fs::path cur = dir;
|
||||
while (true) {
|
||||
// Check each pattern in this directory
|
||||
for (const auto &pat: pats) {
|
||||
if (pat.empty())
|
||||
continue;
|
||||
fs::path candidate = cur / pat;
|
||||
std::error_code ec;
|
||||
bool exists = fs::exists(candidate, ec);
|
||||
if (!ec && exists) {
|
||||
rootCache_[cacheKey] = cur.string();
|
||||
return rootCache_[cacheKey];
|
||||
}
|
||||
}
|
||||
if (cur.has_parent_path()) {
|
||||
fs::path parent = cur.parent_path();
|
||||
if (parent == cur)
|
||||
break; // reached root guard
|
||||
cur = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rootCache_[cacheKey] = {};
|
||||
return {};
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
@@ -15,6 +15,7 @@ class Editor; // fwd
|
||||
#include "DiagnosticStore.h"
|
||||
#include "LspClient.h"
|
||||
#include "LspServerConfig.h"
|
||||
#include "UtfCodec.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class LspManager {
|
||||
@@ -30,6 +31,11 @@ public:
|
||||
|
||||
void stopAllServers();
|
||||
|
||||
// Manual lifecycle controls
|
||||
bool startServerForLanguage(const std::string &languageId, const std::string &rootPath = std::string());
|
||||
|
||||
bool restartServer(const std::string &languageId, const std::string &rootPath = std::string());
|
||||
|
||||
// Document sync (to be called by editor/buffer events)
|
||||
void onBufferOpened(Buffer *buffer);
|
||||
|
||||
@@ -55,6 +61,14 @@ public:
|
||||
debug_ = enabled;
|
||||
}
|
||||
|
||||
|
||||
// Configuration utilities
|
||||
bool toggleAutostart(const std::string &languageId);
|
||||
|
||||
std::vector<std::string> configuredLanguages() const;
|
||||
|
||||
std::vector<std::string> runningLanguages() const;
|
||||
|
||||
private:
|
||||
[[maybe_unused]] Editor *editor_{}; // non-owning
|
||||
DiagnosticDisplay *display_{}; // non-owning
|
||||
@@ -79,6 +93,15 @@ private:
|
||||
void registerDefaultServers();
|
||||
|
||||
static std::string patternToLanguageId(const std::string &pattern);
|
||||
|
||||
// Workspace root detection helpers/cache
|
||||
std::string detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg);
|
||||
|
||||
// key = startDir + "|" + cfg.rootPatterns
|
||||
std::unordered_map<std::string, std::string> rootCache_;
|
||||
|
||||
// Resolve a buffer by its file:// (or untitled:) URI
|
||||
Buffer *findBufferByUri(const std::string &uri);
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
|
||||
@@ -1,21 +1,191 @@
|
||||
/*
|
||||
* LspProcessClient.cc - initial stub implementation
|
||||
* LspProcessClient.cc - process-based LSP client (Phase 1 minimal)
|
||||
*/
|
||||
#include "LspProcessClient.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <thread>
|
||||
|
||||
#include "json.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
LspProcessClient::LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs)
|
||||
: command_(std::move(serverCommand)), args_(std::move(serverArgs)), transport_(new JsonRpcTransport()) {}
|
||||
: command_(std::move(serverCommand)), args_(std::move(serverArgs)), transport_(new JsonRpcTransport())
|
||||
{
|
||||
if (const char *dbg = std::getenv("KTE_LSP_DEBUG"); dbg && *dbg) {
|
||||
debug_ = true;
|
||||
}
|
||||
if (const char *to = std::getenv("KTE_LSP_REQ_TIMEOUT_MS"); to && *to) {
|
||||
char *end = nullptr;
|
||||
long long v = std::strtoll(to, &end, 10);
|
||||
if (end && *end == '\0' && v >= 0) {
|
||||
requestTimeoutMs_ = v;
|
||||
}
|
||||
}
|
||||
if (const char *mp = std::getenv("KTE_LSP_MAX_PENDING"); mp && *mp) {
|
||||
char *end = nullptr;
|
||||
long long v = std::strtoll(mp, &end, 10);
|
||||
if (end && *end == '\0' && v >= 0) {
|
||||
maxPending_ = static_cast<size_t>(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LspProcessClient::~LspProcessClient() = default;
|
||||
LspProcessClient::~LspProcessClient()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::initialize(const std::string &/*rootPath*/)
|
||||
LspProcessClient::spawnServerProcess()
|
||||
{
|
||||
// Phase 1–2: no real process spawn yet
|
||||
int toChild[2]; // parent writes toChild[1] -> child's stdin
|
||||
int fromChild[2]; // child writes fromChild[1] -> parent's stdout reader
|
||||
if (pipe(toChild) != 0) {
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] pipe(toChild) failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (pipe(fromChild) != 0) {
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] pipe(fromChild) failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
// fork failed
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
::close(fromChild[0]);
|
||||
::close(fromChild[1]);
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] fork failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
// Child: set up stdio
|
||||
::dup2(toChild[0], STDIN_FILENO);
|
||||
::dup2(fromChild[1], STDOUT_FILENO);
|
||||
// Close extra fds
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
::close(fromChild[0]);
|
||||
::close(fromChild[1]);
|
||||
// Build argv
|
||||
std::vector<char *> argv;
|
||||
argv.push_back(const_cast<char *>(command_.c_str()));
|
||||
for (auto &s: args_)
|
||||
argv.push_back(const_cast<char *>(s.c_str()));
|
||||
argv.push_back(nullptr);
|
||||
// Exec
|
||||
execvp(command_.c_str(), argv.data());
|
||||
// If exec fails
|
||||
// Note: in child; cannot easily log to parent. Attempt to write to stderr.
|
||||
std::fprintf(stderr, "[kte][lsp] execvp failed for '%s': %s\n", command_.c_str(), std::strerror(errno));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
// Parent: keep ends
|
||||
childPid_ = pid;
|
||||
outFd_ = toChild[1]; // write to child's stdin
|
||||
inFd_ = fromChild[0]; // read from child's stdout
|
||||
// Close the other ends we don't use
|
||||
::close(toChild[0]);
|
||||
::close(fromChild[1]);
|
||||
// Set CLOEXEC on our fds
|
||||
fcntl(outFd_, F_SETFD, FD_CLOEXEC);
|
||||
fcntl(inFd_, F_SETFD, FD_CLOEXEC);
|
||||
if (debug_) {
|
||||
std::ostringstream oss;
|
||||
oss << command_;
|
||||
for (const auto &a: args_) {
|
||||
oss << ' ' << a;
|
||||
}
|
||||
const char *pathEnv = std::getenv("PATH");
|
||||
std::fprintf(stderr, "[kte][lsp] spawned pid=%d argv=[%s] inFd=%d outFd=%d PATH=%s\n",
|
||||
static_cast<int>(childPid_), oss.str().c_str(), inFd_, outFd_,
|
||||
pathEnv ? pathEnv : "<null>");
|
||||
}
|
||||
transport_->connect(inFd_, outFd_);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::terminateProcess()
|
||||
{
|
||||
if (outFd_ >= 0) {
|
||||
::close(outFd_);
|
||||
outFd_ = -1;
|
||||
}
|
||||
if (inFd_ >= 0) {
|
||||
::close(inFd_);
|
||||
inFd_ = -1;
|
||||
}
|
||||
if (childPid_ > 0) {
|
||||
// Try to wait non-blocking; if still running, send SIGTERM
|
||||
int status = 0;
|
||||
pid_t r = waitpid(childPid_, &status, WNOHANG);
|
||||
if (r == 0) {
|
||||
// still running
|
||||
kill(childPid_, SIGTERM);
|
||||
waitpid(childPid_, &status, 0);
|
||||
}
|
||||
childPid_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::sendInitialize(const std::string &rootPath)
|
||||
{
|
||||
int idNum = nextRequestIntId_++;
|
||||
pendingInitializeId_ = std::to_string(idNum);
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = idNum;
|
||||
j["method"] = "initialize";
|
||||
nlohmann::json params;
|
||||
params["processId"] = static_cast<int>(getpid());
|
||||
params["rootUri"] = toFileUri(rootPath);
|
||||
// Minimal client capabilities for now
|
||||
nlohmann::json caps;
|
||||
caps["textDocument"]["synchronization"]["didSave"] = true;
|
||||
params["capabilities"] = std::move(caps);
|
||||
j["params"] = std::move(params);
|
||||
transport_->send("initialize", j.dump());
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::initialize(const std::string &rootPath)
|
||||
{
|
||||
if (running_)
|
||||
return true;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] initialize: rootPath=%s\n", rootPath.c_str());
|
||||
if (!spawnServerProcess())
|
||||
return false;
|
||||
running_ = true;
|
||||
sendInitialize(rootPath);
|
||||
startReader();
|
||||
startTimeoutWatchdog();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -23,37 +193,509 @@ LspProcessClient::initialize(const std::string &/*rootPath*/)
|
||||
void
|
||||
LspProcessClient::shutdown()
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] shutdown\n");
|
||||
// Send shutdown request then exit notification (best-effort)
|
||||
int id = nextRequestIntId_++;
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = id;
|
||||
j["method"] = "shutdown";
|
||||
transport_->send("shutdown", j.dump());
|
||||
}
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "exit";
|
||||
transport_->send("exit", j.dump());
|
||||
}
|
||||
// Close pipes to unblock reader, then join thread, then ensure child is gone
|
||||
terminateProcess();
|
||||
stopReader();
|
||||
stopTimeoutWatchdog();
|
||||
// Clear any pending callbacks
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
pending_.clear();
|
||||
pendingOrder_.clear();
|
||||
}
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didOpen(const std::string &/*uri*/, const std::string &/*languageId*/,
|
||||
int /*version*/, const std::string &/*text*/)
|
||||
LspProcessClient::didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text)
|
||||
{
|
||||
// Stub: would send textDocument/didOpen
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didOpen uri=%s lang=%s version=%d bytes=%zu\n",
|
||||
uri.c_str(), languageId.c_str(), version, text.size());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didOpen";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
j["params"]["textDocument"]["languageId"] = languageId;
|
||||
j["params"]["textDocument"]["version"] = version;
|
||||
j["params"]["textDocument"]["text"] = text;
|
||||
transport_->send("textDocument/didOpen", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didChange(const std::string &/*uri*/, int /*version*/,
|
||||
const std::vector<TextDocumentContentChangeEvent> &/*changes*/)
|
||||
LspProcessClient::didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes)
|
||||
{
|
||||
// Stub: would send textDocument/didChange
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didChange uri=%s version=%d changes=%zu\n",
|
||||
uri.c_str(), version, changes.size());
|
||||
// Phase 1: send full or ranged changes using proper JSON construction
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didChange";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
j["params"]["textDocument"]["version"] = version;
|
||||
auto &arr = j["params"]["contentChanges"];
|
||||
arr = nlohmann::json::array();
|
||||
for (const auto &ch: changes) {
|
||||
nlohmann::json c;
|
||||
if (ch.range.has_value()) {
|
||||
c["range"]["start"]["line"] = ch.range->start.line;
|
||||
c["range"]["start"]["character"] = ch.range->start.character;
|
||||
c["range"]["end"]["line"] = ch.range->end.line;
|
||||
c["range"]["end"]["character"] = ch.range->end.character;
|
||||
}
|
||||
c["text"] = ch.text;
|
||||
arr.push_back(std::move(c));
|
||||
}
|
||||
transport_->send("textDocument/didChange", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didClose(const std::string &/*uri*/)
|
||||
LspProcessClient::didClose(const std::string &uri)
|
||||
{
|
||||
// Stub
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didClose uri=%s\n", uri.c_str());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didClose";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
transport_->send("textDocument/didClose", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didSave(const std::string &/*uri*/)
|
||||
LspProcessClient::didSave(const std::string &uri)
|
||||
{
|
||||
// Stub
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didSave uri=%s\n", uri.c_str());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didSave";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
transport_->send("textDocument/didSave", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::startReader()
|
||||
{
|
||||
stopReader_ = false;
|
||||
reader_ = std::thread([this] {
|
||||
this->readerLoop();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::stopReader()
|
||||
{
|
||||
stopReader_ = true;
|
||||
if (reader_.joinable()) {
|
||||
// Waking up read() by closing inFd_ is handled in terminateProcess(); ensure it’s closed first
|
||||
// Here, best-effort join with small delay
|
||||
reader_.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::readerLoop()
|
||||
{
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] readerLoop start\n");
|
||||
while (!stopReader_) {
|
||||
auto msg = transport_->read();
|
||||
if (!msg.has_value()) {
|
||||
// EOF or error
|
||||
break;
|
||||
}
|
||||
handleIncoming(msg->raw);
|
||||
}
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] readerLoop end\n");
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::handleIncoming(const std::string &json)
|
||||
{
|
||||
try {
|
||||
auto j = nlohmann::json::parse(json, nullptr, false);
|
||||
if (j.is_discarded())
|
||||
return; // malformed JSON
|
||||
// Validate jsonrpc if present
|
||||
if (auto itRpc = j.find("jsonrpc"); itRpc != j.end()) {
|
||||
if (!itRpc->is_string() || *itRpc != "2.0")
|
||||
return;
|
||||
}
|
||||
|
||||
auto normalizeId = [](const nlohmann::json &idVal) -> std::string {
|
||||
if (idVal.is_string())
|
||||
return idVal.get<std::string>();
|
||||
if (idVal.is_number_integer())
|
||||
return std::to_string(idVal.get<long long>());
|
||||
return std::string();
|
||||
};
|
||||
|
||||
// Handle responses (have id and no method) or server -> client requests (have id and method)
|
||||
if (auto itId = j.find("id"); itId != j.end() && !itId->is_null()) {
|
||||
const std::string respIdStr = normalizeId(*itId);
|
||||
|
||||
// If it's a request from server, it will also have a method
|
||||
if (auto itMeth = j.find("method"); itMeth != j.end() && itMeth->is_string()) {
|
||||
const std::string method = *itMeth;
|
||||
if (method == "workspace/configuration") {
|
||||
// Respond with default empty settings array matching requested items length
|
||||
size_t n = 0;
|
||||
if (auto itParams = j.find("params");
|
||||
itParams != j.end() && itParams->is_object()) {
|
||||
if (auto itItems = itParams->find("items");
|
||||
itItems != itParams->end() && itItems->is_array()) {
|
||||
n = itItems->size();
|
||||
}
|
||||
}
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
// echo id type: if original was string, send string; else number
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
arr.push_back(nlohmann::json::object());
|
||||
resp["result"] = std::move(arr);
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
if (method == "window/showMessageRequest") {
|
||||
// Best-effort respond with null result (dismiss)
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
resp["result"] = nullptr;
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
// Unknown server request: respond with MethodNotFound
|
||||
nlohmann::json err;
|
||||
err["code"] = -32601;
|
||||
err["message"] = "Method not found";
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
resp["error"] = std::move(err);
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize handshake special-case
|
||||
if (!pendingInitializeId_.empty() && respIdStr == pendingInitializeId_) {
|
||||
nlohmann::json init;
|
||||
init["jsonrpc"] = "2.0";
|
||||
init["method"] = "initialized";
|
||||
init["params"] = nlohmann::json::object();
|
||||
transport_->send("initialized", init.dump());
|
||||
pendingInitializeId_.clear();
|
||||
}
|
||||
// Dispatcher lookup
|
||||
std::function < void(const nlohmann::json &, const nlohmann::json *) > cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
auto it = pending_.find(respIdStr);
|
||||
if (it != pending_.end()) {
|
||||
cb = it->second.callback;
|
||||
if (it->second.orderIt != pendingOrder_.end()) {
|
||||
pendingOrder_.erase(it->second.orderIt);
|
||||
}
|
||||
pending_.erase(it);
|
||||
}
|
||||
}
|
||||
if (cb) {
|
||||
const nlohmann::json *errPtr = nullptr;
|
||||
const auto itErr = j.find("error");
|
||||
if (itErr != j.end() && itErr->is_object())
|
||||
errPtr = &(*itErr);
|
||||
nlohmann::json result;
|
||||
const auto itRes = j.find("result");
|
||||
if (itRes != j.end())
|
||||
result = *itRes; // may be null
|
||||
cb(result, errPtr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto itMethod = j.find("method");
|
||||
if (itMethod == j.end() || !itMethod->is_string())
|
||||
return;
|
||||
const std::string method = *itMethod;
|
||||
if (method == "window/logMessage") {
|
||||
if (debug_) {
|
||||
const auto itParams = j.find("params");
|
||||
if (itParams != j.end()) {
|
||||
const auto itMsg = itParams->find("message");
|
||||
if (itMsg != itParams->end() && itMsg->is_string()) {
|
||||
std::fprintf(stderr, "[kte][lsp] logMessage: %s\n",
|
||||
itMsg->get_ref<const std::string &>().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method == "window/showMessage") {
|
||||
const auto itParams = j.find("params");
|
||||
if (debug_ &&itParams
|
||||
|
||||
!=
|
||||
j.end() && itParams->is_object()
|
||||
)
|
||||
{
|
||||
int typ = 0;
|
||||
std::string msg;
|
||||
if (auto itm = itParams->find("message"); itm != itParams->end() && itm->is_string())
|
||||
msg = *itm;
|
||||
if (auto ity = itParams->find("type");
|
||||
ity != itParams->end() && ity->is_number_integer())
|
||||
typ = *ity;
|
||||
std::fprintf(stderr, "[kte][lsp] showMessage(type=%d): %s\n", typ, msg.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method != "textDocument/publishDiagnostics") {
|
||||
return;
|
||||
}
|
||||
const auto itParams = j.find("params");
|
||||
if (itParams == j.end() || !itParams->is_object())
|
||||
return;
|
||||
const auto itUri = itParams->find("uri");
|
||||
if (itUri == itParams->end() || !itUri->is_string())
|
||||
return;
|
||||
const std::string uri = *itUri;
|
||||
std::vector<Diagnostic> diags;
|
||||
const auto itDiag = itParams->find("diagnostics");
|
||||
if (itDiag != itParams->end() && itDiag->is_array()) {
|
||||
for (const auto &djson: *itDiag) {
|
||||
if (!djson.is_object())
|
||||
continue;
|
||||
Diagnostic d;
|
||||
// severity
|
||||
int sev = 3;
|
||||
if (auto itS = djson.find("severity"); itS != djson.end() && itS->is_number_integer()) {
|
||||
sev = *itS;
|
||||
}
|
||||
switch (sev) {
|
||||
case 1:
|
||||
d.severity = DiagnosticSeverity::Error;
|
||||
break;
|
||||
case 2:
|
||||
d.severity = DiagnosticSeverity::Warning;
|
||||
break;
|
||||
case 3:
|
||||
d.severity = DiagnosticSeverity::Information;
|
||||
break;
|
||||
case 4:
|
||||
d.severity = DiagnosticSeverity::Hint;
|
||||
break;
|
||||
default:
|
||||
d.severity = DiagnosticSeverity::Information;
|
||||
break;
|
||||
}
|
||||
if (auto itM = djson.find("message"); itM != djson.end() && itM->is_string()) {
|
||||
d.message = *itM;
|
||||
}
|
||||
if (auto itR = djson.find("range"); itR != djson.end() && itR->is_object()) {
|
||||
if (auto itStart = itR->find("start");
|
||||
itStart != itR->end() && itStart->is_object()) {
|
||||
if (auto itL = itStart->find("line");
|
||||
itL != itStart->end() && itL->is_number_integer()) {
|
||||
d.range.start.line = *itL;
|
||||
}
|
||||
if (auto itC = itStart->find("character");
|
||||
itC != itStart->end() && itC->is_number_integer()) {
|
||||
d.range.start.character = *itC;
|
||||
}
|
||||
}
|
||||
if (auto itEnd = itR->find("end"); itEnd != itR->end() && itEnd->is_object()) {
|
||||
if (auto itL = itEnd->find("line");
|
||||
itL != itEnd->end() && itL->is_number_integer()) {
|
||||
d.range.end.line = *itL;
|
||||
}
|
||||
if (auto itC = itEnd->find("character");
|
||||
itC != itEnd->end() && itC->is_number_integer()) {
|
||||
d.range.end.character = *itC;
|
||||
}
|
||||
}
|
||||
}
|
||||
// optional code/source
|
||||
if (auto itCode = djson.find("code"); itCode != djson.end()) {
|
||||
if (itCode->is_string())
|
||||
d.code = itCode->get<std::string>();
|
||||
else if (itCode->is_number_integer())
|
||||
d.code = std::to_string(itCode->get<int>());
|
||||
}
|
||||
if (auto itSrc = djson.find("source"); itSrc != djson.end() && itSrc->is_string()) {
|
||||
d.source = itSrc->get<std::string>();
|
||||
}
|
||||
diags.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
if (diagnosticsHandler_) {
|
||||
diagnosticsHandler_(uri, diags);
|
||||
}
|
||||
} catch (...) {
|
||||
// swallow parse errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
LspProcessClient::sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb)
|
||||
{
|
||||
if (!running_)
|
||||
return 0;
|
||||
int id = nextRequestIntId_++;
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = id;
|
||||
j["method"] = method;
|
||||
if (!params.is_null())
|
||||
j["params"] = params;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> request method=%s id=%d\n", method.c_str(), id);
|
||||
transport_->send(method, j.dump());
|
||||
if (cb) {
|
||||
std::function < void() > callDropped;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
if (maxPending_ > 0 && pending_.size() >= maxPending_) {
|
||||
// Evict oldest
|
||||
if (!pendingOrder_.empty()) {
|
||||
std::string oldestId = pendingOrder_.front();
|
||||
auto it = pending_.find(oldestId);
|
||||
if (it != pending_.end()) {
|
||||
auto cbOld = it->second.callback;
|
||||
std::string methOld = it->second.method;
|
||||
if (debug_) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
"[kte][lsp] dropping oldest pending id=%s method=%s (cap=%zu)\n",
|
||||
oldestId.c_str(), methOld.c_str(), maxPending_);
|
||||
}
|
||||
// Prepare drop callback to run outside lock
|
||||
callDropped = [cbOld] {
|
||||
if (cbOld) {
|
||||
nlohmann::json err;
|
||||
err["code"] = -32001;
|
||||
err["message"] =
|
||||
"Request dropped (max pending exceeded)";
|
||||
cbOld(nlohmann::json(), &err);
|
||||
}
|
||||
};
|
||||
pending_.erase(it);
|
||||
}
|
||||
pendingOrder_.pop_front();
|
||||
}
|
||||
}
|
||||
pendingOrder_.push_back(std::to_string(id));
|
||||
auto itOrder = pendingOrder_.end();
|
||||
--itOrder;
|
||||
PendingRequest pr;
|
||||
pr.method = method;
|
||||
pr.callback = std::move(cb);
|
||||
if (requestTimeoutMs_ > 0) {
|
||||
pr.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(
|
||||
requestTimeoutMs_);
|
||||
}
|
||||
pr.orderIt = itOrder;
|
||||
pending_[std::to_string(id)] = std::move(pr);
|
||||
}
|
||||
if (callDropped)
|
||||
callDropped();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::completion(const std::string &uri, Position pos, CompletionCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/completion", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &/*result*/, const nlohmann::json * /*error*/) {
|
||||
if (cb)
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::hover(const std::string &uri, Position pos, HoverCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/hover", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &/*result*/, const nlohmann::json * /*error*/) {
|
||||
if (cb)
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::definition(const std::string &uri, Position pos, LocationCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/definition", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &/*result*/, const nlohmann::json * /*error*/) {
|
||||
if (cb)
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,4 +711,73 @@ LspProcessClient::getServerName() const
|
||||
{
|
||||
return command_;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspProcessClient::toFileUri(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return std::string();
|
||||
#ifdef _WIN32
|
||||
return std::string("file:/") + path;
|
||||
#else
|
||||
return std::string("file://") + path;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::startTimeoutWatchdog()
|
||||
{
|
||||
stopTimeout_ = false;
|
||||
if (requestTimeoutMs_ <= 0)
|
||||
return;
|
||||
timeoutThread_ = std::thread([this] {
|
||||
while (!stopTimeout_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
struct Expired {
|
||||
std::string id;
|
||||
std::string method;
|
||||
std::function<void(const nlohmann::json &, const nlohmann::json *)> cb;
|
||||
};
|
||||
std::vector<Expired> expired;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
for (auto it = pending_.begin(); it != pending_.end();) {
|
||||
const auto &pr = it->second;
|
||||
if (pr.deadline.time_since_epoch().count() != 0 && now >= pr.deadline) {
|
||||
expired.push_back(Expired{it->first, pr.method, pr.callback});
|
||||
if (pr.orderIt != pendingOrder_.end())
|
||||
pendingOrder_.erase(pr.orderIt);
|
||||
it = pending_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto &kv: expired) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] request timeout id=%s method=%s\n",
|
||||
kv.id.c_str(), kv.method.c_str());
|
||||
}
|
||||
if (kv.cb) {
|
||||
nlohmann::json err;
|
||||
err["code"] = -32000;
|
||||
err["message"] = "Request timed out";
|
||||
kv.cb(nlohmann::json(), &err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::stopTimeoutWatchdog()
|
||||
{
|
||||
stopTimeout_ = true;
|
||||
if (timeoutThread_.joinable())
|
||||
timeoutThread_.join();
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
@@ -7,6 +7,15 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#include <list>
|
||||
|
||||
#include "json.h"
|
||||
|
||||
#include "LspClient.h"
|
||||
#include "JsonRpcTransport.h"
|
||||
@@ -32,15 +41,148 @@ public:
|
||||
|
||||
void didSave(const std::string &uri) override;
|
||||
|
||||
// Language Features (wire-up via dispatcher; minimal callbacks for now)
|
||||
void completion(const std::string &uri, Position pos,
|
||||
CompletionCallback cb) override;
|
||||
|
||||
void hover(const std::string &uri, Position pos,
|
||||
HoverCallback cb) override;
|
||||
|
||||
void definition(const std::string &uri, Position pos,
|
||||
LocationCallback cb) override;
|
||||
|
||||
bool isRunning() const override;
|
||||
|
||||
std::string getServerName() const override;
|
||||
|
||||
|
||||
void setDiagnosticsHandler(DiagnosticsHandler h) override
|
||||
{
|
||||
diagnosticsHandler_ = std::move(h);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string command_;
|
||||
std::vector<std::string> args_;
|
||||
std::unique_ptr<JsonRpcTransport> transport_;
|
||||
bool running_ = false;
|
||||
bool running_ = false;
|
||||
bool debug_ = false;
|
||||
int inFd_ = -1; // read from server (server stdout)
|
||||
int outFd_ = -1; // write to server (server stdin)
|
||||
pid_t childPid_ = -1;
|
||||
int nextRequestIntId_ = 1;
|
||||
std::string pendingInitializeId_{}; // echo exactly as sent (string form)
|
||||
|
||||
// Incoming processing
|
||||
std::thread reader_;
|
||||
std::atomic<bool> stopReader_{false};
|
||||
DiagnosticsHandler diagnosticsHandler_{};
|
||||
|
||||
// Simple request dispatcher: map request id -> callback
|
||||
struct PendingRequest {
|
||||
std::string method;
|
||||
// If error is present, errorJson points to it; otherwise nullptr
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> callback;
|
||||
// Optional timeout
|
||||
std::chrono::steady_clock::time_point deadline{}; // epoch if no timeout
|
||||
// Order tracking for LRU eviction
|
||||
std::list<std::string>::iterator orderIt{};
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, PendingRequest> pending_;
|
||||
// Maintain insertion order (oldest at front)
|
||||
std::list<std::string> pendingOrder_;
|
||||
std::mutex pendingMutex_;
|
||||
|
||||
// Timeout/watchdog for pending requests
|
||||
std::thread timeoutThread_;
|
||||
std::atomic<bool> stopTimeout_{false};
|
||||
int64_t requestTimeoutMs_ = 0; // 0 = disabled
|
||||
size_t maxPending_ = 0; // 0 = unlimited
|
||||
|
||||
bool spawnServerProcess();
|
||||
|
||||
void terminateProcess();
|
||||
|
||||
static std::string toFileUri(const std::string &path);
|
||||
|
||||
void sendInitialize(const std::string &rootPath);
|
||||
|
||||
void startReader();
|
||||
|
||||
void stopReader();
|
||||
|
||||
void readerLoop();
|
||||
|
||||
void handleIncoming(const std::string &json);
|
||||
|
||||
// Helper to send a request with params and register a response callback
|
||||
int sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb);
|
||||
|
||||
// Start/stop timeout thread
|
||||
void startTimeoutWatchdog();
|
||||
|
||||
void stopTimeoutWatchdog();
|
||||
|
||||
public:
|
||||
// Test hook: inject a raw JSON message as if received from server
|
||||
void debugInjectMessageForTest(const std::string &raw)
|
||||
{
|
||||
handleIncoming(raw);
|
||||
}
|
||||
|
||||
|
||||
// Test hook: add a pending request entry manually
|
||||
void debugAddPendingForTest(const std::string &id, const std::string &method,
|
||||
std::function<void(const nlohmann::json & result,
|
||||
const nlohmann::json *errorJson)
|
||||
|
||||
>
|
||||
cb
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
pendingOrder_.push_back(id);
|
||||
auto it = pendingOrder_.end();
|
||||
--it;
|
||||
PendingRequest pr{method, std::move(cb), {}, it};
|
||||
pending_[id] = std::move(pr);
|
||||
}
|
||||
|
||||
|
||||
// Test hook: override timeout
|
||||
void setRequestTimeoutMsForTest(int64_t ms)
|
||||
{
|
||||
requestTimeoutMs_ = ms;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: set max pending
|
||||
void setMaxPendingForTest(size_t maxPending)
|
||||
{
|
||||
maxPending_ = maxPending;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: set running flag (to allow sendRequest in tests without spawning)
|
||||
void setRunningForTest(bool r)
|
||||
{
|
||||
running_ = r;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: send a raw request using internal machinery
|
||||
int debugSendRequestForTest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result,
|
||||
const nlohmann::json *errorJson)
|
||||
|
||||
>
|
||||
cb
|
||||
)
|
||||
{
|
||||
return sendRequest(method, params, std::move(cb));
|
||||
}
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#include <vector>
|
||||
|
||||
namespace kte::lsp {
|
||||
// NOTE on coordinates:
|
||||
// - Internal editor coordinates use UTF-8 columns counted by Unicode scalars.
|
||||
// - LSP wire protocol uses UTF-16 code units for the `character` field.
|
||||
// Conversions are performed in higher layers via `lsp/UtfCodec.h` helpers.
|
||||
struct Position {
|
||||
int line = 0;
|
||||
int character = 0;
|
||||
|
||||
155
lsp/UtfCodec.cc
Normal file
155
lsp/UtfCodec.cc
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* UtfCodec.cc - UTF-8 <-> UTF-16 code unit position conversions
|
||||
*/
|
||||
#include "UtfCodec.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace kte::lsp {
|
||||
// Decode next code point from a UTF-8 string.
|
||||
// On invalid input, consumes 1 byte and returns U+FFFD.
|
||||
// Returns: (codepoint, bytesConsumed)
|
||||
static inline std::pair<uint32_t, size_t>
|
||||
decodeUtf8(std::string_view s, size_t i)
|
||||
{
|
||||
if (i >= s.size())
|
||||
return {0, 0};
|
||||
unsigned char c0 = static_cast<unsigned char>(s[i]);
|
||||
if (c0 < 0x80) {
|
||||
return {c0, 1};
|
||||
}
|
||||
// Determine sequence length
|
||||
if ((c0 & 0xE0) == 0xC0) {
|
||||
if (i + 1 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
if ((c1 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x1F) << 6) | (c1 & 0x3F);
|
||||
// Overlong check: must be >= 0x80
|
||||
if (cp < 0x80)
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 2};
|
||||
}
|
||||
if ((c0 & 0xF0) == 0xE0) {
|
||||
if (i + 2 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
|
||||
// Overlong / surrogate range check
|
||||
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 3};
|
||||
}
|
||||
if ((c0 & 0xF8) == 0xF0) {
|
||||
if (i + 3 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||
unsigned char c3 = static_cast<unsigned char>(s[i + 3]);
|
||||
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
|
||||
// Overlong / max range check
|
||||
if (cp < 0x10000 || cp > 0x10FFFF)
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 4};
|
||||
}
|
||||
return {0xFFFD, 1};
|
||||
}
|
||||
|
||||
|
||||
static inline size_t
|
||||
utf16UnitsForCodepoint(uint32_t cp)
|
||||
{
|
||||
return (cp <= 0xFFFF) ? 1 : 2;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col)
|
||||
{
|
||||
// Count by Unicode scalars up to utf8Col; clamp at EOL
|
||||
size_t units = 0;
|
||||
size_t col = 0;
|
||||
size_t i = 0;
|
||||
while (i < lineUtf8.size()) {
|
||||
if (col >= utf8Col)
|
||||
break;
|
||||
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||
if (n == 0)
|
||||
break;
|
||||
units += utf16UnitsForCodepoint(cp);
|
||||
i += n;
|
||||
++col;
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units)
|
||||
{
|
||||
// Traverse code points until consuming utf16Units (or reaching EOL)
|
||||
size_t units = 0;
|
||||
size_t col = 0;
|
||||
size_t i = 0;
|
||||
while (i < lineUtf8.size()) {
|
||||
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||
if (n == 0)
|
||||
break;
|
||||
size_t add = utf16UnitsForCodepoint(cp);
|
||||
if (units + add > utf16Units)
|
||||
break;
|
||||
units += add;
|
||||
i += n;
|
||||
++col;
|
||||
if (units == utf16Units)
|
||||
break;
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
|
||||
Position
|
||||
toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider)
|
||||
{
|
||||
Position out = pUtf8;
|
||||
std::string_view line = provider ? provider(uri, pUtf8.line) : std::string_view();
|
||||
out.character = static_cast<int>(utf8ColToUtf16Units(line, static_cast<size_t>(pUtf8.character)));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Position
|
||||
toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider)
|
||||
{
|
||||
Position out = pUtf16;
|
||||
std::string_view line = provider ? provider(uri, pUtf16.line) : std::string_view();
|
||||
out.character = static_cast<int>(utf16UnitsToUtf8Col(line, static_cast<size_t>(pUtf16.character)));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Range
|
||||
toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider)
|
||||
{
|
||||
Range r;
|
||||
r.start = toUtf16(uri, rUtf8.start, provider);
|
||||
r.end = toUtf16(uri, rUtf8.end, provider);
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
Range
|
||||
toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider)
|
||||
{
|
||||
Range r;
|
||||
r.start = toUtf8(uri, rUtf16.start, provider);
|
||||
r.end = toUtf8(uri, rUtf16.end, provider);
|
||||
return r;
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
37
lsp/UtfCodec.h
Normal file
37
lsp/UtfCodec.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* UtfCodec.h - Helpers for UTF-8 <-> UTF-16 code unit position conversions
|
||||
*/
|
||||
#ifndef KTE_LSP_UTF_CODEC_H
|
||||
#define KTE_LSP_UTF_CODEC_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "LspTypes.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
// Map between editor-internal UTF-8 columns (by Unicode scalar count)
|
||||
// and LSP wire UTF-16 code units (per LSP spec).
|
||||
|
||||
// Convert a UTF-8 column index (in Unicode scalars) to UTF-16 code units for a given line.
|
||||
size_t utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col);
|
||||
|
||||
// Convert a UTF-16 code unit count to a UTF-8 column index (in Unicode scalars) for a given line.
|
||||
size_t utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units);
|
||||
|
||||
// Line text provider to allow conversions without giving the codec direct buffer access.
|
||||
using LineProvider = std::function<std::string_view(const std::string & uri, int line)>;
|
||||
|
||||
// Convenience helpers for positions and ranges using a line provider.
|
||||
Position toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider);
|
||||
|
||||
Position toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider);
|
||||
|
||||
Range toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider);
|
||||
|
||||
Range toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider);
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_UTF_CODEC_H
|
||||
Reference in New Issue
Block a user