mirror of
				https://github.com/rust-lang/rust-analyzer.git
				synced 2025-10-31 12:04:43 +00:00 
			
		
		
		
	 4541495037
			
		
	
	
		4541495037
		
	
	
	
	
		
			
			* `examples/minimal_lsp.rs` – compact LSP server showing definition, completion, hover, rustfmt-based formatting, and dummy diagnostics. Advertises UTF-8 offset encoding. * `examples/manual_test.sh` – FIFO script that streams the canonical nine LSP packets so anyone can validate the server from two terminals. No new runtime deps; `anyhow` stays under [dev-dependencies].
		
			
				
	
	
		
			335 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			335 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! 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(())
 | ||
| }
 |