//! 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> { 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> { let _init: InitializeParams = serde_json::from_value(params)?; let mut docs: FxHashMap = 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, ) -> 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, ) -> 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 { 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(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(()) }