Files
kte/lsp/LspManager.cc
Kyle Isom 33bbb5b98f 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.
2025-12-02 00:15:15 -08:00

694 lines
20 KiB
C++

/*
* LspManager.cc - central coordination of LSP servers and diagnostics
*/
#include "LspManager.h"
#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)
{
// Pre-populate with sensible default server configs
registerDefaultServers();
}
void
LspManager::registerServer(const std::string &languageId, const LspServerConfig &config)
{
serverConfigs_[languageId] = config;
}
bool
LspManager::startServerForBuffer(Buffer *buffer)
{
const auto lang = getLanguageId(buffer);
if (lang.empty())
return false;
if (servers_.find(lang) != servers_.end() && servers_[lang]->isRunning()) {
return true;
}
auto it = serverConfigs_.find(lang);
if (it == serverConfigs_.end()) {
return false;
}
const auto &cfg = it->second;
// Respect autostart for automatic starts on buffer open
if (!cfg.autostart) {
return false;
}
// 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()) {
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);
return true;
}
void
LspManager::stopServer(const std::string &languageId)
{
auto it = servers_.find(languageId);
if (it != servers_.end()) {
it->second->shutdown();
servers_.erase(it);
}
}
void
LspManager::stopAllServers()
{
for (auto &kv: servers_) {
kv.second->shutdown();
}
servers_.clear();
}
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 (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;
const auto uri = getUri(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);
}
void
LspManager::onBufferChanged(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
const auto uri = getUri(buffer);
int version = static_cast<int>(buffer->Version());
std::vector<TextDocumentContentChangeEvent> changes;
if (auto *tracker = buffer->GetChangeTracker()) {
changes = tracker->getChanges();
tracker->clearChanges();
version = tracker->getVersion();
} else {
// Fallback: full document change
TextDocumentContentChangeEvent ev;
ev.range.reset();
ev.text = buffer->FullText();
changes.push_back(std::move(ev));
}
// 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);
}
void
LspManager::onBufferClosed(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
client->didClose(getUri(buffer));
// Clear diagnostics for this file
diagnosticStore_.clear(getUri(buffer));
}
void
LspManager::onBufferSaved(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
client->didSave(getUri(buffer));
}
void
LspManager::requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
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));
}
}
void
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
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));
}
}
void
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
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));
}
}
void
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &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, 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)
{
// Prefer explicit filetype if set
const auto &ft = buffer->Filetype();
if (!ft.empty())
return ft;
// Otherwise map extension
fs::path p(buffer->Filename());
return extToLanguageId(p.extension().string());
}
std::string
LspManager::getUri(Buffer *buffer)
{
const auto &path = buffer->Filename();
if (path.empty()) {
// Untitled buffer: use a pseudo-URI
return std::string("untitled:") + std::to_string(reinterpret_cast<std::uintptr_t>(buffer));
}
fs::path p(path);
p = fs::weakly_canonical(p);
#ifdef _WIN32
// rudimentary file URI; future: robust encoding
return std::string("file:/") + p.string();
#else
return std::string("file://") + p.string();
#endif
}
// 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)
{
std::string e = ext;
if (!e.empty() && e[0] == '.')
e.erase(0, 1);
std::string lower;
lower.resize(e.size());
std::transform(e.begin(), e.end(), lower.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (lower == "rs")
return "rust";
if (lower == "c" || lower == "cc" || lower == "cpp" || lower == "h" || lower == "hpp")
return "cpp";
if (lower == "go")
return "go";
if (lower == "py")
return "python";
if (lower == "js")
return "javascript";
if (lower == "ts")
return "typescript";
if (lower == "json")
return "json";
if (lower == "sh" || lower == "bash" || lower == "zsh")
return "shell";
if (lower == "md")
return "markdown";
return lower; // best-effort
}
LspClient *
LspManager::ensureServerForLanguage(const std::string &languageId)
{
auto it = servers_.find(languageId);
if (it != servers_.end() && it->second && it->second->isRunning()) {
return it->second.get();
}
// Attempt to start from config if present
auto cfg = serverConfigs_.find(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();
servers_[languageId] = std::move(client);
return ret;
}
void
LspManager::registerDefaultServers()
{
// Import defaults and register by inferred languageId from file patterns
for (const auto &cfg: GetDefaultServerConfigs()) {
if (cfg.filePatterns.empty()) {
// If no patterns, we can't infer; skip
continue;
}
for (const auto &pat: cfg.filePatterns) {
const auto lang = patternToLanguageId(pat);
if (lang.empty())
continue;
// Don't overwrite if user already registered a server for this lang
if (serverConfigs_.find(lang) == serverConfigs_.end()) {
serverConfigs_.emplace(lang, cfg);
}
}
}
}
std::string
LspManager::patternToLanguageId(const std::string &pattern)
{
// Expect patterns like "*.rs", "*.cpp" etc. Extract extension and reuse extToLanguageId
// Find last '.' in the pattern and take substring after it, stripping any trailing wildcards
std::string ext;
// Common case: starts with *.
auto pos = pattern.rfind('.');
if (pos != std::string::npos && pos + 1 < pattern.size()) {
ext = pattern.substr(pos + 1);
// Remove any trailing wildcard characters
while (!ext.empty() && (ext.back() == '*' || ext.back() == '?')) {
ext.pop_back();
}
} else {
// No dot; try to treat whole pattern as extension after trimming leading '*'
ext = pattern;
while (!ext.empty() && (ext.front() == '*' || ext.front() == '.')) {
ext.erase(ext.begin());
}
}
if (ext.empty())
return {};
return extToLanguageId(ext);
}
// 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