feat: implement LSP server for IDE integration

Add a Language Server Protocol (LSP) server to enable IDE integration.
The server provides:

- Real-time diagnostics (parse errors and type errors)
- Basic hover information
- Keyword completions (fn, let, if, match, type, effect, etc.)
- Go-to-definition stub (ready for implementation)

Usage: lux --lsp

The LSP server can be integrated with any editor that supports LSP,
including VS Code, Neovim, Emacs, and others.

Dependencies added:
- lsp-server 0.7
- lsp-types 0.94
- serde with derive feature
- serde_json

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 05:42:37 -05:00
parent 3206aad653
commit 20bf75a5f8
4 changed files with 722 additions and 7 deletions

352
src/lsp.rs Normal file
View File

@@ -0,0 +1,352 @@
//! LSP (Language Server Protocol) server for Lux
//!
//! Provides IDE features like:
//! - Diagnostics (errors and warnings)
//! - Hover information
//! - Go to definition
//! - Completions
use crate::parser::Parser;
use crate::typechecker::TypeChecker;
use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
use lsp_types::{
notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
request::{Completion, GotoDefinition, HoverRequest},
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
HoverProviderCapability, InitializeParams, MarkupContent, MarkupKind, Position,
PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind, Url,
};
use std::collections::HashMap;
use std::error::Error;
/// LSP Server for Lux
pub struct LspServer {
connection: Connection,
/// Document contents by URI
documents: HashMap<Url, String>,
}
impl LspServer {
/// Run the LSP server
pub fn run() -> Result<(), Box<dyn Error + Sync + Send>> {
eprintln!("Starting Lux LSP server...");
// Create the transport connection
let (connection, io_threads) = Connection::stdio();
// Run the server
let server = LspServer {
connection,
documents: HashMap::new(),
};
server.main_loop()?;
// Wait for the I/O threads to finish
io_threads.join()?;
eprintln!("Lux LSP server stopped.");
Ok(())
}
fn main_loop(mut self) -> Result<(), Box<dyn Error + Sync + Send>> {
// Initialize
let server_capabilities = serde_json::to_value(ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
definition_provider: Some(lsp_types::OneOf::Left(true)),
..Default::default()
})?;
let init_params = self.connection.initialize(server_capabilities)?;
let _init_params: InitializeParams = serde_json::from_value(init_params)?;
eprintln!("LSP server initialized");
// Main message loop
loop {
let msg = match self.connection.receiver.recv() {
Ok(msg) => msg,
Err(_) => break, // Channel closed
};
match msg {
Message::Request(req) => {
if self.connection.handle_shutdown(&req)? {
return Ok(());
}
self.handle_request(req)?;
}
Message::Notification(not) => {
self.handle_notification(not)?;
}
Message::Response(_) => {}
}
}
Ok(())
}
fn handle_request(&self, req: Request) -> Result<(), Box<dyn Error + Sync + Send>> {
let req = match cast_request::<HoverRequest>(req) {
Ok((id, params)) => {
let result = self.handle_hover(params);
let resp = Response::new_ok(id, result);
self.connection.sender.send(Message::Response(resp))?;
return Ok(());
}
Err(req) => req,
};
let req = match cast_request::<Completion>(req) {
Ok((id, params)) => {
let result = self.handle_completion(params);
let resp = Response::new_ok(id, result);
self.connection.sender.send(Message::Response(resp))?;
return Ok(());
}
Err(req) => req,
};
let _req = match cast_request::<GotoDefinition>(req) {
Ok((id, params)) => {
let result = self.handle_goto_definition(params);
let resp = Response::new_ok(id, result);
self.connection.sender.send(Message::Response(resp))?;
return Ok(());
}
Err(req) => req,
};
Ok(())
}
fn handle_notification(
&mut self,
not: lsp_server::Notification,
) -> Result<(), Box<dyn Error + Sync + Send>> {
match not.method.as_str() {
DidOpenTextDocument::METHOD => {
let params: DidOpenTextDocumentParams = serde_json::from_value(not.params)?;
let uri = params.text_document.uri;
let text = params.text_document.text;
self.documents.insert(uri.clone(), text.clone());
self.publish_diagnostics(uri, &text)?;
}
DidChangeTextDocument::METHOD => {
let params: DidChangeTextDocumentParams = serde_json::from_value(not.params)?;
let uri = params.text_document.uri;
if let Some(change) = params.content_changes.into_iter().last() {
self.documents.insert(uri.clone(), change.text.clone());
self.publish_diagnostics(uri, &change.text)?;
}
}
_ => {}
}
Ok(())
}
fn publish_diagnostics(
&self,
uri: Url,
text: &str,
) -> Result<(), Box<dyn Error + Sync + Send>> {
let diagnostics = self.get_diagnostics(text);
let params = PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
};
self.connection.sender.send(Message::Notification(
lsp_server::Notification::new(
"textDocument/publishDiagnostics".to_string(),
params,
),
))?;
Ok(())
}
fn get_diagnostics(&self, source: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
// Parse the source
let program = match Parser::parse_source(source) {
Ok(prog) => prog,
Err(err) => {
diagnostics.push(Diagnostic {
range: span_to_range(source, err.span.start, err.span.end),
severity: Some(DiagnosticSeverity::ERROR),
message: err.message,
..Default::default()
});
return diagnostics;
}
};
// Type check
let mut checker = TypeChecker::new();
if let Err(errors) = checker.check_program(&program) {
for error in errors {
diagnostics.push(Diagnostic {
range: span_to_range(source, error.span.start, error.span.end),
severity: Some(DiagnosticSeverity::ERROR),
message: error.message,
..Default::default()
});
}
}
diagnostics
}
fn handle_hover(&self, params: HoverParams) -> Option<Hover> {
let uri = params.text_document_position_params.text_document.uri;
let _position = params.text_document_position_params.position;
let _source = self.documents.get(&uri)?;
// For now, return basic hover info
// A full implementation would find the symbol at the position
// and return its type and documentation
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: "Lux language element".to_string(),
}),
range: None,
})
}
fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
let _uri = params.text_document_position.text_document.uri;
// Return basic completions
// A full implementation would analyze context and provide
// relevant completions based on scope
let items = vec![
CompletionItem {
label: "fn".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Function declaration".to_string()),
..Default::default()
},
CompletionItem {
label: "let".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Variable binding".to_string()),
..Default::default()
},
CompletionItem {
label: "if".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Conditional expression".to_string()),
..Default::default()
},
CompletionItem {
label: "match".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Pattern matching".to_string()),
..Default::default()
},
CompletionItem {
label: "type".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Type declaration".to_string()),
..Default::default()
},
CompletionItem {
label: "effect".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Effect declaration".to_string()),
..Default::default()
},
CompletionItem {
label: "handler".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Effect handler".to_string()),
..Default::default()
},
CompletionItem {
label: "trait".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Trait declaration".to_string()),
..Default::default()
},
CompletionItem {
label: "impl".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Trait implementation".to_string()),
..Default::default()
},
];
Some(CompletionResponse::Array(items))
}
fn handle_goto_definition(
&self,
_params: GotoDefinitionParams,
) -> Option<GotoDefinitionResponse> {
// A full implementation would find the definition location
// of the symbol at the given position
None
}
}
/// Convert byte offsets to LSP Position
fn span_to_range(source: &str, start: usize, end: usize) -> Range {
let start_pos = offset_to_position(source, start);
let end_pos = offset_to_position(source, end);
Range {
start: start_pos,
end: end_pos,
}
}
fn offset_to_position(source: &str, offset: usize) -> Position {
let mut line = 0u32;
let mut col = 0u32;
for (i, c) in source.char_indices() {
if i >= offset {
break;
}
if c == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
Position {
line,
character: col,
}
}
fn cast_request<R>(req: Request) -> Result<(RequestId, R::Params), Request>
where
R: lsp_types::request::Request,
R::Params: serde::de::DeserializeOwned,
{
match req.extract(R::METHOD) {
Ok(params) => Ok(params),
Err(ExtractError::MethodMismatch(req)) => Err(req),
Err(ExtractError::JsonError { .. }) => {
// This shouldn't happen if the client is well-behaved
panic!("JSON deserialization error in LSP request")
}
}
}