Add viewport-aware syntax prefetching and background warming.

- Added prefetching in both terminal and GUI renderers to optimize visible row highlights.
- Introduced background worker for offscreen highlight warming to improve scrolling performance.
- Refactored `HighlighterEngine` to manage thread-safety, caching, and stateful re-computation.
- Integrated changes into `HighlighterEngine`, `TerminalRenderer`, and `GUIRenderer`.
- Bumped version to 1.2.0 in preparation for the release.
This commit is contained in:
2025-12-01 18:37:01 -08:00
parent 1a77f28ce4
commit e62cf3ee28
7 changed files with 737 additions and 72 deletions

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.1.2")
set(KTE_VERSION "1.2.0")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.

View File

@@ -3,6 +3,7 @@
#include <ncurses.h>
#include <SDL.h>
#include <imgui.h>
#include "GUIInputHandler.h"
#include "KKeymap.h"
@@ -284,6 +285,14 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
bool produced = false;
switch (e.type) {
case SDL_MOUSEWHEEL: {
// If ImGui wants to capture the mouse (e.g., hovering the File Picker list),
// don't translate wheel events into editor scrolling.
// This prevents background buffer scroll while using GUI widgets.
ImGuiIO &io = ImGui::GetIO();
if (io.WantCaptureMouse) {
return true; // consumed by GUI
}
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED

View File

@@ -139,23 +139,29 @@ GUIRenderer::Draw(Editor &ed)
vis_rows = 1;
long last_row = first_row + vis_rows - 1;
if (!forced_scroll) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
float max_y = ImGui::GetScrollMaxY();
if (target < 0.f)
target = 0.f;
if (max_y >= 0.f && target > max_y)
target = max_y;
ImGui::SetScrollY(target);
// refresh local variables
scroll_y = ImGui::GetScrollY();
first_row = static_cast<long>(scroll_y / row_h);
last_row = first_row + vis_rows - 1;
}
}
}
if (!forced_scroll) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
float max_y = ImGui::GetScrollMaxY();
if (target < 0.f)
target = 0.f;
if (max_y >= 0.f && target > max_y)
target = max_y;
ImGui::SetScrollY(target);
// refresh local variables
scroll_y = ImGui::GetScrollY();
first_row = static_cast<long>(scroll_y / row_h);
last_row = first_row + vis_rows - 1;
}
}
// Phase 3: prefetch visible viewport highlights and warm around in background
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(std::max(0L, first_row));
int rc = static_cast<int>(std::max(1L, vis_rows));
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
}
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;

View File

@@ -1,84 +1,112 @@
#include "HighlighterEngine.h"
#include "Buffer.h"
#include "LanguageHighlighter.h"
#include <thread>
namespace kte {
HighlighterEngine::HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine()
{
// stop background worker
if (worker_running_.load()) {
{
std::lock_guard<std::mutex> lock(mtx_);
worker_running_.store(false);
has_request_ = true; // wake it up to exit
}
cv_.notify_one();
if (worker_.joinable()) worker_.join();
}
}
void
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
{
std::lock_guard<std::mutex> lock(mtx_);
hl_ = std::move(hl);
cache_.clear();
state_cache_.clear();
state_last_contig_.clear();
}
const LineHighlight &
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
{
std::unique_lock<std::mutex> lock(mtx_);
auto it = cache_.find(row);
if (it != cache_.end()) {
if (it->second.version == buf_version) {
return it->second;
}
if (it != cache_.end() && it->second.version == buf_version) {
return it->second;
}
LineHighlight updated;
updated.version = buf_version;
updated.spans.clear();
// Prepare destination slot to reuse its capacity and avoid allocations
LineHighlight &slot = cache_[row];
slot.version = buf_version;
slot.spans.clear();
if (!hl_) {
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
return slot;
}
if (auto *stateful = dynamic_cast<StatefulHighlighter *>(hl_.get())) {
// Find nearest cached state at or before row-1 with matching version
StatefulHighlighter::LineState prev_state;
int start_row = -1;
if (!state_cache_.empty()) {
// linear search over map (unordered), track best candidate
int best = -1;
for (const auto &kv : state_cache_) {
int r = kv.first;
if (r <= row - 1 && kv.second.version == buf_version) {
if (r > best) {
best = r;
}
}
}
if (best >= 0) {
start_row = best;
prev_state = state_cache_.at(best).state;
}
}
// Copy shared_ptr-like raw pointer for use outside critical sections
LanguageHighlighter *hl_ptr = hl_.get();
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
// Walk from start_row+1 up to row computing states; only collect spans at the target row
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? updated.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, prev_state, out);
// store state for this row (state after finishing r)
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
prev_state = next_state;
}
} else {
// Stateless path
hl_->HighlightLine(buf, row, updated.spans);
if (!is_stateful) {
// Stateless fast path: we can release the lock while computing to reduce contention
auto &out = slot.spans;
lock.unlock();
hl_ptr->HighlightLine(buf, row, out);
return cache_.at(row);
}
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
// but release during heavy computation.
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
StatefulHighlighter::LineState prev_state;
int start_row = -1;
if (!state_cache_.empty()) {
// linear search over map (unordered), track best candidate
int best = -1;
for (const auto &kv : state_cache_) {
int r = kv.first;
if (r <= row - 1 && kv.second.version == buf_version) {
if (r > best) best = r;
}
}
if (best >= 0) {
start_row = best;
prev_state = state_cache_.at(best).state;
}
}
// We'll compute states and the target line's spans without holding the lock for most of the work.
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
lock.unlock();
StatefulHighlighter::LineState cur_state = prev_state;
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
// Update state cache for r
std::lock_guard<std::mutex> gl(mtx_);
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
cur_state = next_state;
}
// Return reference under lock to ensure slot's address stability in map
lock.lock();
return cache_.at(row);
}
void
HighlighterEngine::InvalidateFrom(int row)
{
std::lock_guard<std::mutex> lock(mtx_);
if (cache_.empty()) return;
// Simple implementation: erase all rows >= row
for (auto it = cache_.begin(); it != cache_.end(); ) {
@@ -91,4 +119,63 @@ HighlighterEngine::InvalidateFrom(int row)
}
}
void HighlighterEngine::ensure_worker_started() const
{
if (worker_running_.load()) return;
worker_running_.store(true);
worker_ = std::thread([this]() { this->worker_loop(); });
}
void HighlighterEngine::worker_loop() const
{
std::unique_lock<std::mutex> lock(mtx_);
while (worker_running_.load()) {
cv_.wait(lock, [this]() { return has_request_ || !worker_running_.load(); });
if (!worker_running_.load()) break;
WarmRequest req = pending_;
has_request_ = false;
// Copy locals then release lock while computing
lock.unlock();
if (req.buf) {
int start = std::max(0, req.start_row);
int end = std::max(start, req.end_row);
for (int r = start; r <= end; ++r) {
// Re-check version staleness quickly by peeking cache version; not strictly necessary
// Compute line; GetLine is thread-safe
(void)this->GetLine(*req.buf, r, req.version);
}
}
lock.lock();
}
}
void HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version, int warm_margin) const
{
if (row_count <= 0) return;
// Synchronously compute visible rows to ensure cache hits during draw
int start = std::max(0, first_row);
int end = start + row_count - 1;
int max_rows = static_cast<int>(buf.Nrows());
if (start >= max_rows) return;
if (end >= max_rows) end = max_rows - 1;
for (int r = start; r <= end; ++r) {
(void)GetLine(buf, r, buf_version);
}
// Enqueue background warm-around
int warm_start = std::max(0, start - warm_margin);
int warm_end = std::min(max_rows - 1, end + warm_margin);
{
std::lock_guard<std::mutex> lock(mtx_);
pending_.buf = &buf;
pending_.version = buf_version;
pending_.start_row = warm_start;
pending_.end_row = warm_end;
has_request_ = true;
}
ensure_worker_started();
cv_.notify_one();
}
} // namespace kte

View File

@@ -5,6 +5,10 @@
#include <memory>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
#include "Highlight.h"
#include "LanguageHighlighter.h"
@@ -29,6 +33,11 @@ public:
bool HasHighlighter() const { return static_cast<bool>(hl_); }
// Phase 3: viewport-first prefetch and background warming
// Compute only the visible range now, and enqueue a background warm-around task.
// warm_margin: how many extra lines above/below to warm in the background.
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version, int warm_margin = 200) const;
private:
std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine)
@@ -40,6 +49,28 @@ private:
StatefulHighlighter::LineState state;
};
mutable std::unordered_map<int, StateEntry> state_cache_;
// Track best known contiguous state row for a given version to avoid O(n) scans
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
// Thread-safety for caches and background worker state
mutable std::mutex mtx_;
// Background warmer
struct WarmRequest {
const Buffer *buf{nullptr};
std::uint64_t version{0};
int start_row{0};
int end_row{0}; // inclusive
};
mutable std::condition_variable cv_;
mutable std::thread worker_;
mutable std::atomic<bool> worker_running_{false};
mutable bool has_request_{false};
mutable WarmRequest pending_{};
void ensure_worker_started() const;
void worker_loop() const;
};
} // namespace kte

View File

@@ -42,11 +42,18 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs();
const int tabw = 8;
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
// Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs);
int rc = std::max(0, content_rows);
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)

525
docs/lsp plan.md Normal file
View File

@@ -0,0 +1,525 @@
# LSP Support Implementation Plan for kte
## Executive Summary
This plan outlines a comprehensive approach to integrating Language Server Protocol (LSP) support into kte while
respecting its core architectural principles: **frontend/backend separation**, **testability**, and **dual terminal/GUI
support**.
---
## 1. Core Architecture
### 1.1 LSP Client Module Structure
```c++
// LspClient.h - Core LSP client abstraction
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
virtual void completion(const std::string& uri, Position pos,
CompletionCallback callback) = 0;
virtual void hover(const std::string& uri, Position pos,
HoverCallback callback) = 0;
virtual void definition(const std::string& uri, Position pos,
LocationCallback callback) = 0;
virtual void references(const std::string& uri, Position pos,
LocationsCallback callback) = 0;
virtual void diagnostics(DiagnosticsCallback callback) = 0;
// Process Management
virtual bool isRunning() const = 0;
virtual std::string getServerName() const = 0;
};
```
### 1.2 Process-based LSP Implementation
```c++
// LspProcessClient.h - Manages LSP server subprocess
class LspProcessClient : public LspClient {
private:
std::string serverCommand_;
std::vector<std::string> serverArgs_;
std::unique_ptr<Process> process_;
std::unique_ptr<JsonRpcTransport> transport_;
std::unordered_map<int, PendingRequest> pendingRequests_;
int nextRequestId_ = 1;
// Async I/O handling
std::thread readerThread_;
std::mutex mutex_;
std::condition_variable cv_;
public:
LspProcessClient(const std::string& command,
const std::vector<std::string>& args);
// ... implementation of LspClient interface
};
```
### 1.3 JSON-RPC Transport Layer
```c++
// JsonRpcTransport.h
class JsonRpcTransport {
public:
// Send a request and get the request ID
int sendRequest(const std::string& method, const nlohmann::json& params);
// Send a notification (no response expected)
void sendNotification(const std::string& method, const nlohmann::json& params);
// Read next message (blocking)
std::optional<JsonRpcMessage> readMessage();
private:
void writeMessage(const nlohmann::json& message);
std::string readContentLength();
int fdIn_; // stdin to server
int fdOut_; // stdout from server
};
```
---
## 2. Incremental Document Updates
### 2.1 Change Tracking in Buffer
The key to efficient LSP integration is tracking changes incrementally. This integrates with the existing `Buffer`
class:
```c++
// TextDocumentContentChangeEvent.h
struct TextDocumentContentChangeEvent {
std::optional<Range> range; // If nullopt, entire document changed
std::optional<int> rangeLength; // Deprecated but some servers use it
std::string text;
};
// BufferChangeTracker.h - Integrates with Buffer to track changes
class BufferChangeTracker {
public:
explicit BufferChangeTracker(Buffer* buffer);
// Called by Buffer on each edit operation
void recordInsertion(Position pos, const std::string& text);
void recordDeletion(Range range, const std::string& deletedText);
// Get accumulated changes since last sync
std::vector<TextDocumentContentChangeEvent> getChanges();
// Clear changes after sending to LSP
void clearChanges();
// Get current document version
int getVersion() const { return version_; }
private:
Buffer* buffer_;
int version_ = 0;
std::vector<TextDocumentContentChangeEvent> pendingChanges_;
// Optional: Coalesce adjacent changes
void coalesceChanges();
};
```
### 2.2 Integration with Buffer Operations
```c++
// Buffer.h additions
class Buffer {
// ... existing code ...
// LSP integration
void setChangeTracker(std::unique_ptr<BufferChangeTracker> tracker);
BufferChangeTracker* getChangeTracker() { return changeTracker_.get(); }
// These methods should call tracker when present
void insertText(Position pos, const std::string& text);
void deleteRange(Range range);
private:
std::unique_ptr<BufferChangeTracker> changeTracker_;
};
```
### 2.3 Sync Strategy Selection
```c++
// LspSyncMode.h
enum class LspSyncMode {
None, // No sync
Full, // Send full document on each change
Incremental // Send only changes (preferred)
};
// Determined during server capability negotiation
LspSyncMode negotiateSyncMode(const ServerCapabilities& caps);
```
---
## 3. Diagnostics Display System
### 3.1 Diagnostic Data Model
```c++
// Diagnostic.h
enum class DiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
};
struct Diagnostic {
Range range;
DiagnosticSeverity severity;
std::optional<std::string> code;
std::optional<std::string> source;
std::string message;
std::vector<DiagnosticRelatedInformation> relatedInfo;
};
// DiagnosticStore.h - Central storage for diagnostics
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();
// Statistics
int getErrorCount(const std::string& uri) const;
int getWarningCount(const std::string& uri) const;
private:
std::unordered_map<std::string, std::vector<Diagnostic>> diagnostics_;
};
```
### 3.2 Frontend-Agnostic Diagnostic Display Interface
Following kte's existing abstraction pattern with `Frontend`, `Renderer`, and `InputHandler`:
```c++
// DiagnosticDisplay.h - Abstract interface for showing diagnostics
class DiagnosticDisplay {
public:
virtual ~DiagnosticDisplay() = default;
// Update the diagnostic indicators for a buffer
virtual void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) = 0;
// Show inline diagnostic at cursor position
virtual void showInlineDiagnostic(const Diagnostic& diagnostic) = 0;
// Show diagnostic list/panel
virtual void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) = 0;
virtual void hideDiagnosticList() = 0;
// Status bar summary
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
};
```
### 3.3 Terminal Diagnostic Display
```c++
// TerminalDiagnosticDisplay.h
class TerminalDiagnosticDisplay : 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:
TerminalRenderer* renderer_;
// Terminal-specific display strategies
void renderGutterMarkers(const std::vector<Diagnostic>& diagnostics);
void renderUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderVirtualText(const Diagnostic& diagnostic);
};
```
**Terminal Display Strategies:**
1. **Gutter markers**: Show `E` (error), `W` (warning), `I` (info), `H` (hint) in left gutter
2. **Underlines**: Use terminal underline/curly underline capabilities (where supported)
3. **Virtual text**: Display diagnostic message at end of line (configurable)
4. **Status line**: `[E:3 W:5]` summary
5. **Message line**: Full diagnostic on cursor line shown in bottom bar
```
1 │ fn main() {
E 2 │ let x: i32 = "hello";
3 │ }
──────────────────────────────────────
error[E0308]: mismatched types
expected `i32`, found `&str`
[E:1 W:0] main.rs
```
### 3.4 GUI Diagnostic Display
```c++
// GUIDiagnosticDisplay.h
class GUIDiagnosticDisplay : public DiagnosticDisplay {
public:
explicit GUIDiagnosticDisplay(GUIRenderer* renderer, GUITheme* theme);
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:
GUIRenderer* renderer_;
GUITheme* theme_;
// GUI-specific display
void renderWavyUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderTooltip(Position pos, const Diagnostic& diagnostic);
void renderDiagnosticPanel();
};
```
**GUI Display Features:**
1. **Wavy underlines**: Classic IDE-style (red for errors, yellow for warnings, etc.)
2. **Gutter icons**: Colored icons/dots in the gutter
3. **Hover tooltips**: Rich tooltips on hover showing full diagnostic
4. **Diagnostic panel**: Bottom panel with clickable diagnostic list
5. **Minimap markers**: Colored marks on the minimap (if present)
---
## 4. LspManager - Central Coordination
```c++
// LspManager.h
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
void onBufferOpened(Buffer* buffer);
void onBufferChanged(Buffer* buffer);
void onBufferClosed(Buffer* buffer);
void onBufferSaved(Buffer* buffer);
// Feature requests
void requestCompletion(Buffer* buffer, Position pos,
CompletionCallback callback);
void requestHover(Buffer* buffer, Position pos,
HoverCallback callback);
void requestDefinition(Buffer* buffer, Position pos,
LocationCallback callback);
// Configuration
void setDebugLogging(bool enabled);
private:
Editor* editor_;
DiagnosticDisplay* display_;
DiagnosticStore diagnosticStore_;
std::unordered_map<std::string, std::unique_ptr<LspClient>> servers_;
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
void handleDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics);
std::string getLanguageId(Buffer* buffer);
std::string getUri(Buffer* buffer);
};
```
---
## 5. Configuration
```c++
// LspServerConfig.h
struct LspServerConfig {
std::string command;
std::vector<std::string> args;
std::vector<std::string> filePatterns; // e.g., {"*.rs", "*.toml"}
std::string rootPatterns; // e.g., "Cargo.toml"
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
bool autostart = true;
std::unordered_map<std::string, nlohmann::json> initializationOptions;
std::unordered_map<std::string, nlohmann::json> settings;
};
// Default configurations
std::vector<LspServerConfig> getDefaultServerConfigs() {
return {
{
.command = "rust-analyzer",
.filePatterns = {"*.rs"},
.rootPatterns = "Cargo.toml"
},
{
.command = "clangd",
.args = {"--background-index"},
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
.rootPatterns = "compile_commands.json"
},
{
.command = "gopls",
.filePatterns = {"*.go"},
.rootPatterns = "go.mod"
},
// ... more servers
};
}
```
---
## 6. Implementation Phases
### Phase 1: Foundation (2-3 weeks)
- [ ] JSON-RPC transport layer
- [ ] Process management for LSP servers
- [ ] Basic `LspClient` with initialize/shutdown
- [ ] `textDocument/didOpen`, `textDocument/didClose` (full sync)
### Phase 2: Incremental Sync (1-2 weeks)
- [ ] `BufferChangeTracker` integration with `Buffer`
- [ ] `textDocument/didChange` with incremental updates
- [ ] Change coalescing for rapid edits
- [ ] Version tracking
### Phase 3: Diagnostics (2-3 weeks)
- [ ] `DiagnosticStore` implementation
- [ ] `TerminalDiagnosticDisplay` with gutter markers & status line
- [ ] `GUIDiagnosticDisplay` with wavy underlines & tooltips
- [ ] `textDocument/publishDiagnostics` handling
### Phase 4: Language Features (3-4 weeks)
- [ ] Completion (`textDocument/completion`)
- [ ] Hover (`textDocument/hover`)
- [ ] Go to definition (`textDocument/definition`)
- [ ] Find references (`textDocument/references`)
- [ ] Code actions (`textDocument/codeAction`)
### Phase 5: Polish & Advanced Features (2-3 weeks)
- [ ] Multiple server support
- [ ] Server auto-detection
- [ ] Configuration file support
- [ ] Workspace symbol search
- [ ] Rename refactoring
---
## 7. Alignment with kte Core Principles
### 7.1 Frontend/Backend Separation
- LSP logic is completely separate from display
- `DiagnosticDisplay` interface allows identical behavior across Terminal/GUI
- Follows existing pattern: `Renderer`, `InputHandler`, `Frontend`
### 7.2 Testability
- `LspClient` is abstract, enabling `MockLspClient` for testing
- `DiagnosticDisplay` can be mocked for testing diagnostic flow
- Change tracking can be unit tested in isolation
### 7.3 Performance
- Incremental sync minimizes data sent to LSP servers
- Async message handling doesn't block UI
- Diagnostic rendering is batched
### 7.4 Simplicity
- Minimal dependencies (nlohmann/json for JSON handling)
- Self-contained process management
- Clear separation of concerns
---
## 8. File Organization
```
kte/
├── lsp/
│ ├── LspClient.h
│ ├── LspProcessClient.h
│ ├── LspProcessClient.cc
│ ├── LspManager.h
│ ├── LspManager.cc
│ ├── LspServerConfig.h
│ ├── JsonRpcTransport.h
│ ├── JsonRpcTransport.cc
│ ├── LspTypes.h # Position, Range, Location, etc.
│ ├── Diagnostic.h
│ ├── DiagnosticStore.h
│ ├── DiagnosticStore.cc
│ └── BufferChangeTracker.h
├── diagnostic/
│ ├── DiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.cc
│ ├── GUIDiagnosticDisplay.h
│ └── GUIDiagnosticDisplay.cc
```
---
## 9. Dependencies
- **nlohmann/json**: JSON parsing/serialization (header-only)
- **POSIX/Windows process APIs**: For spawning LSP servers
- Existing kte infrastructure: `Buffer`, `Renderer`, `Frontend`, etc.
---
This plan provides a solid foundation for LSP support while maintaining kte's clean architecture. The key insight is
that LSP is fundamentally a backend feature that should be displayed through the existing frontend abstraction layer,
ensuring consistent behavior across terminal and GUI modes.