LSP integration steps 1-4, part of 5.
This commit is contained in:
49
lsp/BufferChangeTracker.cc
Normal file
49
lsp/BufferChangeTracker.cc
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* BufferChangeTracker.cc - minimal initial implementation
|
||||
*/
|
||||
#include "BufferChangeTracker.h"
|
||||
#include "../Buffer.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
BufferChangeTracker::BufferChangeTracker(const Buffer *buffer)
|
||||
: buffer_(buffer) {}
|
||||
|
||||
|
||||
void
|
||||
BufferChangeTracker::recordInsertion(int /*row*/, int /*col*/, const std::string &/*text*/)
|
||||
{
|
||||
// For Phase 1–2 bring-up, coalesce to full-document changes
|
||||
fullChangePending_ = true;
|
||||
++version_;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BufferChangeTracker::recordDeletion(int /*row*/, int /*col*/, std::size_t /*len*/)
|
||||
{
|
||||
fullChangePending_ = true;
|
||||
++version_;
|
||||
}
|
||||
|
||||
|
||||
std::vector<TextDocumentContentChangeEvent>
|
||||
BufferChangeTracker::getChanges() const
|
||||
{
|
||||
std::vector<TextDocumentContentChangeEvent> v;
|
||||
if (!buffer_)
|
||||
return v;
|
||||
if (fullChangePending_) {
|
||||
TextDocumentContentChangeEvent ev;
|
||||
ev.text = buffer_->FullText();
|
||||
v.push_back(std::move(ev));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BufferChangeTracker::clearChanges()
|
||||
{
|
||||
fullChangePending_ = false;
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
44
lsp/BufferChangeTracker.h
Normal file
44
lsp/BufferChangeTracker.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* BufferChangeTracker.h - integrates with Buffer to accumulate LSP-friendly changes
|
||||
*/
|
||||
#ifndef KTE_BUFFER_CHANGE_TRACKER_H
|
||||
#define KTE_BUFFER_CHANGE_TRACKER_H
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "LspTypes.h"
|
||||
|
||||
class Buffer; // forward declare from core
|
||||
|
||||
namespace kte::lsp {
|
||||
class BufferChangeTracker {
|
||||
public:
|
||||
explicit BufferChangeTracker(const Buffer *buffer);
|
||||
|
||||
// Called by Buffer on each edit operation
|
||||
void recordInsertion(int row, int col, const std::string &text);
|
||||
|
||||
void recordDeletion(int row, int col, std::size_t len);
|
||||
|
||||
// Get accumulated changes since last sync
|
||||
std::vector<TextDocumentContentChangeEvent> getChanges() const;
|
||||
|
||||
// Clear changes after sending to LSP
|
||||
void clearChanges();
|
||||
|
||||
// Get current document version for LSP
|
||||
int getVersion() const
|
||||
{
|
||||
return version_;
|
||||
}
|
||||
|
||||
private:
|
||||
const Buffer *buffer_ = nullptr;
|
||||
bool fullChangePending_ = false;
|
||||
int version_ = 0;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_BUFFER_CHANGE_TRACKER_H
|
||||
37
lsp/Diagnostic.h
Normal file
37
lsp/Diagnostic.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Diagnostic.h - LSP diagnostic data types
|
||||
*/
|
||||
#ifndef KTE_LSP_DIAGNOSTIC_H
|
||||
#define KTE_LSP_DIAGNOSTIC_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "LspTypes.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
enum class DiagnosticSeverity {
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Information = 3,
|
||||
Hint = 4
|
||||
};
|
||||
|
||||
struct DiagnosticRelatedInformation {
|
||||
std::string uri; // related location URI
|
||||
Range range; // related range
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct Diagnostic {
|
||||
Range range{};
|
||||
DiagnosticSeverity severity{DiagnosticSeverity::Information};
|
||||
std::optional<std::string> code;
|
||||
std::optional<std::string> source;
|
||||
std::string message;
|
||||
std::vector<DiagnosticRelatedInformation> relatedInfo;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_DIAGNOSTIC_H
|
||||
30
lsp/DiagnosticDisplay.h
Normal file
30
lsp/DiagnosticDisplay.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* DiagnosticDisplay.h - Abstract interface for showing diagnostics
|
||||
*/
|
||||
#ifndef KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||
#define KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Diagnostic.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class DiagnosticDisplay {
|
||||
public:
|
||||
virtual ~DiagnosticDisplay() = default;
|
||||
|
||||
virtual void updateDiagnostics(const std::string &uri,
|
||||
const std::vector<Diagnostic> &diagnostics) = 0;
|
||||
|
||||
virtual void showInlineDiagnostic(const Diagnostic &diagnostic) = 0;
|
||||
|
||||
virtual void showDiagnosticList(const std::vector<Diagnostic> &diagnostics) = 0;
|
||||
|
||||
virtual void hideDiagnosticList() = 0;
|
||||
|
||||
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||
123
lsp/DiagnosticStore.cc
Normal file
123
lsp/DiagnosticStore.cc
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* DiagnosticStore.cc - implementation
|
||||
*/
|
||||
#include "DiagnosticStore.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace kte::lsp {
|
||||
void
|
||||
DiagnosticStore::setDiagnostics(const std::string &uri, std::vector<Diagnostic> diagnostics)
|
||||
{
|
||||
diagnostics_[uri] = std::move(diagnostics);
|
||||
}
|
||||
|
||||
|
||||
const std::vector<Diagnostic> &
|
||||
DiagnosticStore::getDiagnostics(const std::string &uri) const
|
||||
{
|
||||
auto it = diagnostics_.find(uri);
|
||||
static const std::vector<Diagnostic> kEmpty;
|
||||
if (it == diagnostics_.end())
|
||||
return kEmpty;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
|
||||
std::vector<Diagnostic>
|
||||
DiagnosticStore::getDiagnosticsAtLine(const std::string &uri, int line) const
|
||||
{
|
||||
std::vector<Diagnostic> out;
|
||||
auto it = diagnostics_.find(uri);
|
||||
if (it == diagnostics_.end())
|
||||
return out;
|
||||
out.reserve(it->second.size());
|
||||
for (const auto &d: it->second) {
|
||||
if (containsLine(d.range, line))
|
||||
out.push_back(d);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
std::optional<Diagnostic>
|
||||
DiagnosticStore::getDiagnosticAtPosition(const std::string &uri, Position pos) const
|
||||
{
|
||||
auto it = diagnostics_.find(uri);
|
||||
if (it == diagnostics_.end())
|
||||
return std::nullopt;
|
||||
for (const auto &d: it->second) {
|
||||
if (containsPosition(d.range, pos))
|
||||
return d;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DiagnosticStore::clear(const std::string &uri)
|
||||
{
|
||||
diagnostics_.erase(uri);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
DiagnosticStore::clearAll()
|
||||
{
|
||||
diagnostics_.clear();
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
DiagnosticStore::getErrorCount(const std::string &uri) const
|
||||
{
|
||||
auto it = diagnostics_.find(uri);
|
||||
if (it == diagnostics_.end())
|
||||
return 0;
|
||||
int count = 0;
|
||||
for (const auto &d: it->second) {
|
||||
if (d.severity == DiagnosticSeverity::Error)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
DiagnosticStore::getWarningCount(const std::string &uri) const
|
||||
{
|
||||
auto it = diagnostics_.find(uri);
|
||||
if (it == diagnostics_.end())
|
||||
return 0;
|
||||
int count = 0;
|
||||
for (const auto &d: it->second) {
|
||||
if (d.severity == DiagnosticSeverity::Warning)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
DiagnosticStore::containsLine(const Range &r, int line)
|
||||
{
|
||||
return (line > r.start.line || line == r.start.line) &&
|
||||
(line < r.end.line || line == r.end.line);
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
DiagnosticStore::containsPosition(const Range &r, const Position &p)
|
||||
{
|
||||
if (p.line < r.start.line || p.line > r.end.line)
|
||||
return false;
|
||||
if (r.start.line == r.end.line) {
|
||||
return p.line == r.start.line && p.character >= r.start.character && p.character <= r.end.character;
|
||||
}
|
||||
if (p.line == r.start.line)
|
||||
return p.character >= r.start.character;
|
||||
if (p.line == r.end.line)
|
||||
return p.character <= r.end.character;
|
||||
return true; // between start and end lines
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
42
lsp/DiagnosticStore.h
Normal file
42
lsp/DiagnosticStore.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* DiagnosticStore.h - Central storage for diagnostics by document URI
|
||||
*/
|
||||
#ifndef KTE_LSP_DIAGNOSTIC_STORE_H
|
||||
#define KTE_LSP_DIAGNOSTIC_STORE_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Diagnostic.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class DiagnosticStore {
|
||||
public:
|
||||
void setDiagnostics(const std::string &uri, std::vector<Diagnostic> diagnostics);
|
||||
|
||||
const std::vector<Diagnostic> &getDiagnostics(const std::string &uri) const;
|
||||
|
||||
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string &uri, int line) const;
|
||||
|
||||
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string &uri, Position pos) const;
|
||||
|
||||
void clear(const std::string &uri);
|
||||
|
||||
void clearAll();
|
||||
|
||||
int getErrorCount(const std::string &uri) const;
|
||||
|
||||
int getWarningCount(const std::string &uri) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::vector<Diagnostic> > diagnostics_;
|
||||
|
||||
static bool containsLine(const Range &r, int line);
|
||||
|
||||
static bool containsPosition(const Range &r, const Position &p);
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_DIAGNOSTIC_STORE_H
|
||||
19
lsp/JsonRpcTransport.cc
Normal file
19
lsp/JsonRpcTransport.cc
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* JsonRpcTransport.cc - placeholder
|
||||
*/
|
||||
#include "JsonRpcTransport.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
void
|
||||
JsonRpcTransport::send(const std::string &/*method*/, const std::string &/*payload*/)
|
||||
{
|
||||
// stub: no-op
|
||||
}
|
||||
|
||||
|
||||
std::optional<JsonRpcMessage>
|
||||
JsonRpcTransport::read()
|
||||
{
|
||||
return std::nullopt; // stub
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
29
lsp/JsonRpcTransport.h
Normal file
29
lsp/JsonRpcTransport.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* JsonRpcTransport.h - placeholder transport for JSON-RPC over stdio (stub)
|
||||
*/
|
||||
#ifndef KTE_JSON_RPC_TRANSPORT_H
|
||||
#define KTE_JSON_RPC_TRANSPORT_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace kte::lsp {
|
||||
struct JsonRpcMessage {
|
||||
std::string raw; // raw JSON payload (stub)
|
||||
};
|
||||
|
||||
class JsonRpcTransport {
|
||||
public:
|
||||
JsonRpcTransport() = default;
|
||||
|
||||
~JsonRpcTransport() = default;
|
||||
|
||||
// Send a method call (request or notification) - stub does nothing
|
||||
void send(const std::string &method, const std::string &payload);
|
||||
|
||||
// Blocking read next message (stub => returns nullopt)
|
||||
std::optional<JsonRpcMessage> read();
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_JSON_RPC_TRANSPORT_H
|
||||
61
lsp/LspClient.h
Normal file
61
lsp/LspClient.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* LspClient.h - Core LSP client abstraction (initial stub)
|
||||
*/
|
||||
#ifndef KTE_LSP_CLIENT_H
|
||||
#define KTE_LSP_CLIENT_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "LspTypes.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
// Callback types (stubs for future phases)
|
||||
using CompletionCallback = std::function<void()>;
|
||||
using HoverCallback = std::function<void()>;
|
||||
using LocationCallback = std::function<void()>;
|
||||
|
||||
class LspClient {
|
||||
public:
|
||||
virtual ~LspClient() = default;
|
||||
|
||||
// Lifecycle
|
||||
virtual bool initialize(const std::string &rootPath) = 0;
|
||||
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
// Document Synchronization
|
||||
virtual void didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text) = 0;
|
||||
|
||||
virtual void didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes) = 0;
|
||||
|
||||
virtual void didClose(const std::string &uri) = 0;
|
||||
|
||||
virtual void didSave(const std::string &uri) = 0;
|
||||
|
||||
// Language Features (not yet implemented)
|
||||
virtual void completion(const std::string &, Position,
|
||||
CompletionCallback) {}
|
||||
|
||||
|
||||
virtual void hover(const std::string &, Position,
|
||||
HoverCallback) {}
|
||||
|
||||
|
||||
virtual void definition(const std::string &, Position,
|
||||
LocationCallback) {}
|
||||
|
||||
|
||||
// Process Management
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
virtual std::string getServerName() const = 0;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_CLIENT_H
|
||||
326
lsp/LspManager.cc
Normal file
326
lsp/LspManager.cc
Normal file
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* LspManager.cc - central coordination of LSP servers and diagnostics
|
||||
*/
|
||||
|
||||
#include "LspManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
#include "../Buffer.h"
|
||||
#include "../Editor.h"
|
||||
#include "BufferChangeTracker.h"
|
||||
#include "LspProcessClient.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace kte::lsp {
|
||||
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;
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(cfg.command, cfg.args);
|
||||
// Determine root as parent of file for now; future: walk rootPatterns
|
||||
std::string rootPath;
|
||||
if (!buffer->Filename().empty()) {
|
||||
fs::path p(buffer->Filename());
|
||||
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
|
||||
}
|
||||
if (!client->initialize(rootPath)) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferOpened(Buffer *buffer)
|
||||
{
|
||||
if (!startServerForBuffer(buffer))
|
||||
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();
|
||||
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));
|
||||
}
|
||||
client->didChange(uri, version, changes);
|
||||
}
|
||||
|
||||
|
||||
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))) {
|
||||
client->completion(getUri(buffer), pos, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
client->hover(getUri(buffer), pos, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
client->definition(getUri(buffer), pos, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
|
||||
{
|
||||
diagnosticStore_.setDiagnostics(uri, diagnostics);
|
||||
if (display_) {
|
||||
display_->updateDiagnostics(uri, diagnostics);
|
||||
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
85
lsp/LspManager.h
Normal file
85
lsp/LspManager.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* LspManager.h - central coordination of LSP servers and diagnostics
|
||||
*/
|
||||
#ifndef KTE_LSP_MANAGER_H
|
||||
#define KTE_LSP_MANAGER_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class Buffer; // fwd
|
||||
class Editor; // fwd
|
||||
|
||||
#include "DiagnosticDisplay.h"
|
||||
#include "DiagnosticStore.h"
|
||||
#include "LspClient.h"
|
||||
#include "LspServerConfig.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class LspManager {
|
||||
public:
|
||||
explicit LspManager(Editor *editor, DiagnosticDisplay *display);
|
||||
|
||||
// Server management
|
||||
void registerServer(const std::string &languageId, const LspServerConfig &config);
|
||||
|
||||
bool startServerForBuffer(Buffer *buffer);
|
||||
|
||||
void stopServer(const std::string &languageId);
|
||||
|
||||
void stopAllServers();
|
||||
|
||||
// Document sync (to be called by editor/buffer events)
|
||||
void onBufferOpened(Buffer *buffer);
|
||||
|
||||
void onBufferChanged(Buffer *buffer);
|
||||
|
||||
void onBufferClosed(Buffer *buffer);
|
||||
|
||||
void onBufferSaved(Buffer *buffer);
|
||||
|
||||
// Feature requests (stubs)
|
||||
void requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback);
|
||||
|
||||
void requestHover(Buffer *buffer, Position pos, HoverCallback callback);
|
||||
|
||||
void requestDefinition(Buffer *buffer, Position pos, LocationCallback callback);
|
||||
|
||||
// Diagnostics (public so LspClient impls can forward results here later)
|
||||
void handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics);
|
||||
|
||||
|
||||
void setDebugLogging(bool enabled)
|
||||
{
|
||||
debug_ = enabled;
|
||||
}
|
||||
|
||||
private:
|
||||
[[maybe_unused]] Editor *editor_{}; // non-owning
|
||||
DiagnosticDisplay *display_{}; // non-owning
|
||||
DiagnosticStore diagnosticStore_{};
|
||||
|
||||
// Key: languageId → client
|
||||
std::unordered_map<std::string, std::unique_ptr<LspClient> > servers_;
|
||||
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
|
||||
|
||||
// Helpers
|
||||
static std::string getLanguageId(Buffer *buffer);
|
||||
|
||||
static std::string getUri(Buffer *buffer);
|
||||
|
||||
static std::string extToLanguageId(const std::string &ext);
|
||||
|
||||
LspClient *ensureServerForLanguage(const std::string &languageId);
|
||||
|
||||
bool debug_ = false;
|
||||
|
||||
// Configuration helpers
|
||||
void registerDefaultServers();
|
||||
|
||||
static std::string patternToLanguageId(const std::string &pattern);
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_MANAGER_H
|
||||
72
lsp/LspProcessClient.cc
Normal file
72
lsp/LspProcessClient.cc
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* LspProcessClient.cc - initial stub implementation
|
||||
*/
|
||||
#include "LspProcessClient.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()) {}
|
||||
|
||||
|
||||
LspProcessClient::~LspProcessClient() = default;
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::initialize(const std::string &/*rootPath*/)
|
||||
{
|
||||
// Phase 1–2: no real process spawn yet
|
||||
running_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::shutdown()
|
||||
{
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didOpen(const std::string &/*uri*/, const std::string &/*languageId*/,
|
||||
int /*version*/, const std::string &/*text*/)
|
||||
{
|
||||
// Stub: would send textDocument/didOpen
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didChange(const std::string &/*uri*/, int /*version*/,
|
||||
const std::vector<TextDocumentContentChangeEvent> &/*changes*/)
|
||||
{
|
||||
// Stub: would send textDocument/didChange
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didClose(const std::string &/*uri*/)
|
||||
{
|
||||
// Stub
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didSave(const std::string &/*uri*/)
|
||||
{
|
||||
// Stub
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::isRunning() const
|
||||
{
|
||||
return running_;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspProcessClient::getServerName() const
|
||||
{
|
||||
return command_;
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
47
lsp/LspProcessClient.h
Normal file
47
lsp/LspProcessClient.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* LspProcessClient.h - process-based LSP client (initial stub)
|
||||
*/
|
||||
#ifndef KTE_LSP_PROCESS_CLIENT_H
|
||||
#define KTE_LSP_PROCESS_CLIENT_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "LspClient.h"
|
||||
#include "JsonRpcTransport.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class LspProcessClient : public LspClient {
|
||||
public:
|
||||
LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs);
|
||||
|
||||
~LspProcessClient() override;
|
||||
|
||||
bool initialize(const std::string &rootPath) override;
|
||||
|
||||
void shutdown() override;
|
||||
|
||||
void didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text) override;
|
||||
|
||||
void didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes) override;
|
||||
|
||||
void didClose(const std::string &uri) override;
|
||||
|
||||
void didSave(const std::string &uri) override;
|
||||
|
||||
bool isRunning() const override;
|
||||
|
||||
std::string getServerName() const override;
|
||||
|
||||
private:
|
||||
std::string command_;
|
||||
std::vector<std::string> args_;
|
||||
std::unique_ptr<JsonRpcTransport> transport_;
|
||||
bool running_ = false;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_PROCESS_CLIENT_H
|
||||
47
lsp/LspServerConfig.h
Normal file
47
lsp/LspServerConfig.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* LspServerConfig.h - per-language LSP server configuration
|
||||
*/
|
||||
#ifndef KTE_LSP_SERVER_CONFIG_H
|
||||
#define KTE_LSP_SERVER_CONFIG_H
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace kte::lsp {
|
||||
enum class LspSyncMode {
|
||||
None = 0,
|
||||
Full = 1,
|
||||
Incremental = 2,
|
||||
};
|
||||
|
||||
struct LspServerConfig {
|
||||
std::string command; // executable name/path
|
||||
std::vector<std::string> args; // CLI args
|
||||
std::vector<std::string> filePatterns; // e.g. {"*.rs"}
|
||||
std::string rootPatterns; // e.g. "Cargo.toml"
|
||||
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
|
||||
bool autostart = true;
|
||||
std::unordered_map<std::string, std::string> initializationOptions; // placeholder
|
||||
std::unordered_map<std::string, std::string> settings; // placeholder
|
||||
};
|
||||
|
||||
// Provide a small set of defaults; callers may ignore
|
||||
inline std::vector<LspServerConfig>
|
||||
GetDefaultServerConfigs()
|
||||
{
|
||||
return std::vector<LspServerConfig>{
|
||||
LspServerConfig{
|
||||
.command = "rust-analyzer", .args = {}, .filePatterns = {"*.rs"}, .rootPatterns = "Cargo.toml"
|
||||
},
|
||||
LspServerConfig{
|
||||
.command = "clangd", .args = {"--background-index"},
|
||||
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
|
||||
.rootPatterns = "compile_commands.json"
|
||||
},
|
||||
LspServerConfig{.command = "gopls", .args = {}, .filePatterns = {"*.go"}, .rootPatterns = "go.mod"},
|
||||
};
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_SERVER_CONFIG_H
|
||||
29
lsp/LspTypes.h
Normal file
29
lsp/LspTypes.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* LspTypes.h - minimal LSP-related data types for initial integration
|
||||
*/
|
||||
#ifndef KTE_LSP_TYPES_H
|
||||
#define KTE_LSP_TYPES_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace kte::lsp {
|
||||
struct Position {
|
||||
int line = 0;
|
||||
int character = 0;
|
||||
};
|
||||
|
||||
struct Range {
|
||||
Position start;
|
||||
Position end;
|
||||
};
|
||||
|
||||
struct TextDocumentContentChangeEvent {
|
||||
std::optional<Range> range; // if not set, represents full document change
|
||||
std::string text; // new text for the given range
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_TYPES_H
|
||||
53
lsp/TerminalDiagnosticDisplay.cc
Normal file
53
lsp/TerminalDiagnosticDisplay.cc
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* TerminalDiagnosticDisplay.cc - minimal stub implementation
|
||||
*/
|
||||
#include "TerminalDiagnosticDisplay.h"
|
||||
|
||||
#include "../TerminalRenderer.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
TerminalDiagnosticDisplay::TerminalDiagnosticDisplay(TerminalRenderer *renderer)
|
||||
: renderer_(renderer) {}
|
||||
|
||||
|
||||
void
|
||||
TerminalDiagnosticDisplay::updateDiagnostics(const std::string &uri,
|
||||
const std::vector<Diagnostic> &diagnostics)
|
||||
{
|
||||
(void) uri;
|
||||
(void) diagnostics;
|
||||
// Stub: no rendering yet. Future: gutter markers, underlines, virtual text.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TerminalDiagnosticDisplay::showInlineDiagnostic(const Diagnostic &diagnostic)
|
||||
{
|
||||
(void) diagnostic;
|
||||
// Stub: show as message line in future.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TerminalDiagnosticDisplay::showDiagnosticList(const std::vector<Diagnostic> &diagnostics)
|
||||
{
|
||||
(void) diagnostics;
|
||||
// Stub: open a panel/list in future.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TerminalDiagnosticDisplay::hideDiagnosticList()
|
||||
{
|
||||
// Stub
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TerminalDiagnosticDisplay::updateStatusBar(int errorCount, int warningCount)
|
||||
{
|
||||
(void) errorCount;
|
||||
(void) warningCount;
|
||||
// Stub: integrate with status bar rendering later.
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
35
lsp/TerminalDiagnosticDisplay.h
Normal file
35
lsp/TerminalDiagnosticDisplay.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* TerminalDiagnosticDisplay.h - Terminal (ncurses) diagnostics visualization stub
|
||||
*/
|
||||
#ifndef KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||
#define KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "DiagnosticDisplay.h"
|
||||
|
||||
class TerminalRenderer; // fwd
|
||||
|
||||
namespace kte::lsp {
|
||||
class TerminalDiagnosticDisplay final : public DiagnosticDisplay {
|
||||
public:
|
||||
explicit TerminalDiagnosticDisplay(TerminalRenderer *renderer);
|
||||
|
||||
void updateDiagnostics(const std::string &uri,
|
||||
const std::vector<Diagnostic> &diagnostics) override;
|
||||
|
||||
void showInlineDiagnostic(const Diagnostic &diagnostic) override;
|
||||
|
||||
void showDiagnosticList(const std::vector<Diagnostic> &diagnostics) override;
|
||||
|
||||
void hideDiagnosticList() override;
|
||||
|
||||
void updateStatusBar(int errorCount, int warningCount) override;
|
||||
|
||||
private:
|
||||
[[maybe_unused]] TerminalRenderer *renderer_{}; // non-owning
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||
Reference in New Issue
Block a user