| //! 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 lsp_types::{ | |
| CompletionItem, CompletionItemKind, CompletionOptions, CompletionRequest, CompletionResponse, | |
| Contents, DefinitionRequest, Diagnostic, DiagnosticSeverity, DidChangeTextDocumentNotification, | |
| DidChangeTextDocumentParams, DidOpenTextDocumentNotification, DidOpenTextDocumentParams, | |
| DocumentFormattingParams, DocumentFormattingRequest, Hover, HoverProvider, HoverRequest, | |
| InitializeParams, LspNotificationMethod, LspRequestMethod, MarkupContent, Notification, | |
| Position, PublishDiagnosticsNotification, PublishDiagnosticsParams, Range, Request, | |
| ServerCapabilities, TextDocumentSync, TextEdit, Uri, | |
| }; | |
| 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}; | |
| // ===================================================================== | |
| // 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(TextDocumentSync::Kind(lsp_types::TextDocumentSyncKind::Full)), | |
| completion_provider: Some(CompletionOptions::default()), | |
| definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)), | |
| hover_provider: Some(HoverProvider::Bool(true)), | |
| document_formatting_provider: Some(lsp_types::DocumentFormattingProvider::Bool(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<Uri, 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<Uri, String>, | |
| ) -> Result<()> { | |
| let method: LspNotificationMethod<'_> = note.method.as_str().into(); | |
| match method { | |
| DidOpenTextDocumentNotification::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)?; | |
| } | |
| DidChangeTextDocumentNotification::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.text_document_identifier.uri; | |
| let text = match change { | |
| lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangePartial(partial) => partial.text, | |
| lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(whole) => whole.text, | |
| }; | |
| docs.insert(uri.clone(), text); | |
| publish_dummy_diag(conn, &uri)?; | |
| } | |
| } | |
| _ => {} | |
| } | |
| Ok(()) | |
| } | |
| // ===================================================================== | |
| // requests | |
| // ===================================================================== | |
| fn handle_request( | |
| conn: &Connection, | |
| req: &ServerRequest, | |
| docs: &mut FxHashMap<Uri, String>, | |
| ) -> Result<()> { | |
| let parsed: LspRequestMethod<'_> = req.method.as_str().into(); | |
| match parsed { | |
| DefinitionRequest::METHOD => { | |
| send_ok( | |
| conn, | |
| req.id.clone(), | |
| &lsp_types::DefinitionResponse::DefinitionLinkList(Vec::new()), | |
| )?; | |
| } | |
| CompletionRequest::METHOD => { | |
| let item = CompletionItem { | |
| label: "HelloFromLSP".into(), | |
| kind: Some(CompletionItemKind::Function), | |
| detail: Some("dummy completion".into()), | |
| ..Default::default() | |
| }; | |
| let items = vec![item]; | |
| let completion_list = CompletionResponse::CompletionList(lsp_types::CompletionList { | |
| is_incomplete: false, | |
| item_defaults: None, | |
| apply_kind: None, | |
| items, | |
| }); | |
| send_ok(conn, req.id.clone(), &completion_list)?; | |
| } | |
| HoverRequest::METHOD => { | |
| let hover = Hover { | |
| contents: Contents::MarkupContent(MarkupContent { | |
| value: "Hello from *minimal_lsp*".into(), | |
| kind: lsp_types::MarkupKind::Markdown, | |
| }), | |
| range: None, | |
| }; | |
| send_ok(conn, req.id.clone(), &hover)?; | |
| } | |
| DocumentFormattingRequest::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: &Uri) -> 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( | |
| PublishDiagnosticsNotification::METHOD.into(), | |
| 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, response_result: Ok(serde_json::to_value(result)?) }; | |
| 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, | |
| response_result: Err(lsp_server::ResponseError { | |
| code: code as i32, | |
| message: msg.into(), | |
| data: None, | |
| }), | |
| }; | |
| conn.sender.send(Message::Response(resp))?; | |
| Ok(()) | |
| } |