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:
352
src/lsp.rs
Normal file
352
src/lsp.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user