Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'lib/lsp-server/examples/minimal_lsp.rs')
| -rw-r--r-- | lib/lsp-server/examples/minimal_lsp.rs | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/lib/lsp-server/examples/minimal_lsp.rs b/lib/lsp-server/examples/minimal_lsp.rs new file mode 100644 index 0000000000..5eef999e06 --- /dev/null +++ b/lib/lsp-server/examples/minimal_lsp.rs @@ -0,0 +1,335 @@ +//! Minimal Language‑Server‑Protocol example: **`minimal_lsp.rs`** +//! ============================================================= +//! +//! | ↔ / ← | LSP method | What the implementation does | +//! |-------|------------|------------------------------| +//! | ↔ | `initialize` / `initialized` | capability handshake | +//! | ← | `textDocument/publishDiagnostics` | pushes a dummy info diagnostic whenever the buffer changes | +//! | ← | `textDocument/definition` | echoes an empty location array so the jump works | +//! | ← | `textDocument/completion` | offers one hard‑coded item `HelloFromLSP` | +//! | ← | `textDocument/hover` | shows *Hello from minimal_lsp* markdown | +//! | ← | `textDocument/formatting` | pipes the doc through **rustfmt** and returns a full‑file edit | +//! +//! ### Quick start +//! ```bash +//! cd rust-analyzer/lib/lsp-server +//! cargo run --example minimal_lsp +//! ``` +//! +//! ### Minimal manual session (all nine packets) +//! ```no_run +//! # 1. initialize - server replies with capabilities +//! Content-Length: 85 + +//! {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}} +//! +//! # 2. initialized - no response expected +//! Content-Length: 59 + +//! {"jsonrpc":"2.0","method":"initialized","params":{}} +//! +//! # 3. didOpen - provide initial buffer text +//! Content-Length: 173 + +//! {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///tmp/foo.rs","languageId":"rust","version":1,"text":"fn main( ){println!(\"hi\") }"}}} +//! +//! # 4. completion - expect HelloFromLSP +//! Content-Length: 139 + +//! {"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}} +//! +//! # 5. hover - expect markdown greeting +//! Content-Length: 135 + +//! {"jsonrpc":"2.0","id":3,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}} +//! +//! # 6. goto-definition - dummy empty array +//! Content-Length: 139 + +//! {"jsonrpc":"2.0","id":4,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}} +//! +//! # 7. formatting - rustfmt full document +//! Content-Length: 157 + +//! {"jsonrpc":"2.0","id":5,"method":"textDocument/formatting","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"options":{"tabSize":4,"insertSpaces":true}}} +//! +//! # 8. shutdown request - server acks and prepares to exit +//! Content-Length: 67 + +//! {"jsonrpc":"2.0","id":6,"method":"shutdown","params":null} +//! +//! # 9. exit notification - terminates the server +//! Content-Length: 54 + +//! {"jsonrpc":"2.0","method":"exit","params":null} +//! ``` +//! + +use std::{error::Error, io::Write}; + +use rustc_hash::FxHashMap; // fast hash map +use std::process::Stdio; +use toolchain::command; // clippy-approved wrapper + +#[allow(clippy::print_stderr, clippy::disallowed_types, clippy::disallowed_methods)] +use anyhow::{Context, Result, anyhow, bail}; +use lsp_server::{Connection, Message, Request as ServerRequest, RequestId, Response}; +use lsp_types::notification::Notification as _; // for METHOD consts +use lsp_types::request::Request as _; +use lsp_types::{ + CompletionItem, + CompletionItemKind, + // capability helpers + CompletionOptions, + CompletionResponse, + Diagnostic, + DiagnosticSeverity, + DidChangeTextDocumentParams, + DidOpenTextDocumentParams, + DocumentFormattingParams, + Hover, + HoverContents, + HoverProviderCapability, + // core + InitializeParams, + MarkedString, + OneOf, + Position, + PublishDiagnosticsParams, + Range, + ServerCapabilities, + TextDocumentSyncCapability, + TextDocumentSyncKind, + TextEdit, + Url, + // notifications + notification::{DidChangeTextDocument, DidOpenTextDocument, PublishDiagnostics}, + // requests + request::{Completion, Formatting, GotoDefinition, HoverRequest}, +}; // for METHOD consts + +// ===================================================================== +// main +// ===================================================================== + +#[allow(clippy::print_stderr)] +fn main() -> std::result::Result<(), Box<dyn Error + Sync + Send>> { + log::error!("starting minimal_lsp"); + + // transport + let (connection, io_thread) = Connection::stdio(); + + // advertised capabilities + let caps = ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), + completion_provider: Some(CompletionOptions::default()), + definition_provider: Some(OneOf::Left(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + document_formatting_provider: Some(OneOf::Left(true)), + ..Default::default() + }; + let init_value = serde_json::json!({ + "capabilities": caps, + "offsetEncoding": ["utf-8"], + }); + + let init_params = connection.initialize(init_value)?; + main_loop(connection, init_params)?; + io_thread.join()?; + log::error!("shutting down server"); + Ok(()) +} + +// ===================================================================== +// event loop +// ===================================================================== + +fn main_loop( + connection: Connection, + params: serde_json::Value, +) -> std::result::Result<(), Box<dyn Error + Sync + Send>> { + let _init: InitializeParams = serde_json::from_value(params)?; + let mut docs: FxHashMap<Url, String> = FxHashMap::default(); + + for msg in &connection.receiver { + match msg { + Message::Request(req) => { + if connection.handle_shutdown(&req)? { + break; + } + if let Err(err) = handle_request(&connection, &req, &mut docs) { + log::error!("[lsp] request {} failed: {err}", &req.method); + } + } + Message::Notification(note) => { + if let Err(err) = handle_notification(&connection, ¬e, &mut docs) { + log::error!("[lsp] notification {} failed: {err}", note.method); + } + } + Message::Response(resp) => log::error!("[lsp] response: {resp:?}"), + } + } + Ok(()) +} + +// ===================================================================== +// notifications +// ===================================================================== + +fn handle_notification( + conn: &Connection, + note: &lsp_server::Notification, + docs: &mut FxHashMap<Url, String>, +) -> Result<()> { + match note.method.as_str() { + DidOpenTextDocument::METHOD => { + let p: DidOpenTextDocumentParams = serde_json::from_value(note.params.clone())?; + let uri = p.text_document.uri; + docs.insert(uri.clone(), p.text_document.text); + publish_dummy_diag(conn, &uri)?; + } + DidChangeTextDocument::METHOD => { + let p: DidChangeTextDocumentParams = serde_json::from_value(note.params.clone())?; + if let Some(change) = p.content_changes.into_iter().next() { + let uri = p.text_document.uri; + docs.insert(uri.clone(), change.text); + publish_dummy_diag(conn, &uri)?; + } + } + _ => {} + } + Ok(()) +} + +// ===================================================================== +// requests +// ===================================================================== + +fn handle_request( + conn: &Connection, + req: &ServerRequest, + docs: &mut FxHashMap<Url, String>, +) -> Result<()> { + match req.method.as_str() { + GotoDefinition::METHOD => { + send_ok(conn, req.id.clone(), &lsp_types::GotoDefinitionResponse::Array(Vec::new()))?; + } + Completion::METHOD => { + let item = CompletionItem { + label: "HelloFromLSP".into(), + kind: Some(CompletionItemKind::FUNCTION), + detail: Some("dummy completion".into()), + ..Default::default() + }; + send_ok(conn, req.id.clone(), &CompletionResponse::Array(vec![item]))?; + } + HoverRequest::METHOD => { + let hover = Hover { + contents: HoverContents::Scalar(MarkedString::String( + "Hello from *minimal_lsp*".into(), + )), + range: None, + }; + send_ok(conn, req.id.clone(), &hover)?; + } + Formatting::METHOD => { + let p: DocumentFormattingParams = serde_json::from_value(req.params.clone())?; + let uri = p.text_document.uri; + let text = docs + .get(&uri) + .ok_or_else(|| anyhow!("document not in cache – did you send DidOpen?"))?; + let formatted = run_rustfmt(text)?; + let edit = TextEdit { range: full_range(text), new_text: formatted }; + send_ok(conn, req.id.clone(), &vec![edit])?; + } + _ => send_err( + conn, + req.id.clone(), + lsp_server::ErrorCode::MethodNotFound, + "unhandled method", + )?, + } + Ok(()) +} + +// ===================================================================== +// diagnostics +// ===================================================================== +fn publish_dummy_diag(conn: &Connection, uri: &Url) -> Result<()> { + let diag = Diagnostic { + range: Range::new(Position::new(0, 0), Position::new(0, 1)), + severity: Some(DiagnosticSeverity::INFORMATION), + code: None, + code_description: None, + source: Some("minimal_lsp".into()), + message: "dummy diagnostic".into(), + related_information: None, + tags: None, + data: None, + }; + let params = + PublishDiagnosticsParams { uri: uri.clone(), diagnostics: vec![diag], version: None }; + conn.sender.send(Message::Notification(lsp_server::Notification::new( + PublishDiagnostics::METHOD.to_owned(), + params, + )))?; + Ok(()) +} + +// ===================================================================== +// helpers +// ===================================================================== + +fn run_rustfmt(input: &str) -> Result<String> { + let cwd = std::env::current_dir().expect("can't determine CWD"); + let mut child = command("rustfmt", &cwd, &FxHashMap::default()) + .arg("--emit") + .arg("stdout") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to spawn rustfmt – is it installed?")?; + + let Some(stdin) = child.stdin.as_mut() else { + bail!("stdin unavailable"); + }; + stdin.write_all(input.as_bytes())?; + let output = child.wait_with_output()?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("rustfmt failed: {stderr}"); + } + Ok(String::from_utf8(output.stdout)?) +} + +fn full_range(text: &str) -> Range { + let last_line_idx = text.lines().count().saturating_sub(1) as u32; + let last_col = text.lines().last().map_or(0, |l| l.chars().count()) as u32; + Range::new(Position::new(0, 0), Position::new(last_line_idx, last_col)) +} + +fn send_ok<T: serde::Serialize>(conn: &Connection, id: RequestId, result: &T) -> Result<()> { + let resp = Response { id, result: Some(serde_json::to_value(result)?), error: None }; + conn.sender.send(Message::Response(resp))?; + Ok(()) +} + +fn send_err( + conn: &Connection, + id: RequestId, + code: lsp_server::ErrorCode, + msg: &str, +) -> Result<()> { + let resp = Response { + id, + result: None, + error: Some(lsp_server::ResponseError { + code: code as i32, + message: msg.into(), + data: None, + }), + }; + conn.sender.send(Message::Response(resp))?; + Ok(()) +} |