about summary refs log tree commit diff
path: root/src/tools/rust-analyzer/lib/lsp-server/examples
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/rust-analyzer/lib/lsp-server/examples')
-rw-r--r--src/tools/rust-analyzer/lib/lsp-server/examples/goto_def.rs132
-rwxr-xr-xsrc/tools/rust-analyzer/lib/lsp-server/examples/manual_test.sh53
-rw-r--r--src/tools/rust-analyzer/lib/lsp-server/examples/minimal_lsp.rs335
3 files changed, 388 insertions, 132 deletions
diff --git a/src/tools/rust-analyzer/lib/lsp-server/examples/goto_def.rs b/src/tools/rust-analyzer/lib/lsp-server/examples/goto_def.rs
deleted file mode 100644
index 6b3acda7bcd..00000000000
--- a/src/tools/rust-analyzer/lib/lsp-server/examples/goto_def.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-//! A minimal example LSP server that can only respond to the `gotoDefinition` request. To use
-//! this example, execute it and then send an `initialize` request.
-//!
-//! ```no_run
-//! Content-Length: 85
-//!
-//! {"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {"capabilities": {}}}
-//! ```
-//!
-//! This will respond with a server response. Then send it a `initialized` notification which will
-//! have no response.
-//!
-//! ```no_run
-//! Content-Length: 59
-//!
-//! {"jsonrpc": "2.0", "method": "initialized", "params": {}}
-//! ```
-//!
-//! Once these two are sent, then we enter the main loop of the server. The only request this
-//! example can handle is `gotoDefinition`:
-//!
-//! ```no_run
-//! Content-Length: 159
-//!
-//! {"jsonrpc": "2.0", "method": "textDocument/definition", "id": 2, "params": {"textDocument": {"uri": "file://temp"}, "position": {"line": 1, "character": 1}}}
-//! ```
-//!
-//! To finish up without errors, send a shutdown request:
-//!
-//! ```no_run
-//! Content-Length: 67
-//!
-//! {"jsonrpc": "2.0", "method": "shutdown", "id": 3, "params": null}
-//! ```
-//!
-//! The server will exit the main loop and finally we send a `shutdown` notification to stop
-//! the server.
-//!
-//! ```
-//! Content-Length: 54
-//!
-//! {"jsonrpc": "2.0", "method": "exit", "params": null}
-//! ```
-
-#![allow(clippy::print_stderr)]
-
-use std::error::Error;
-
-use lsp_types::OneOf;
-use lsp_types::{
-    GotoDefinitionResponse, InitializeParams, ServerCapabilities, request::GotoDefinition,
-};
-
-use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
-
-fn main() -> Result<(), Box<dyn Error + Sync + Send>> {
-    // Note that  we must have our logging only write out to stderr.
-    eprintln!("starting generic LSP server");
-
-    // Create the transport. Includes the stdio (stdin and stdout) versions but this could
-    // also be implemented to use sockets or HTTP.
-    let (connection, io_threads) = Connection::stdio();
-
-    // Run the server and wait for the two threads to end (typically by trigger LSP Exit event).
-    let server_capabilities = serde_json::to_value(&ServerCapabilities {
-        definition_provider: Some(OneOf::Left(true)),
-        ..Default::default()
-    })
-    .unwrap();
-    let initialization_params = match connection.initialize(server_capabilities) {
-        Ok(it) => it,
-        Err(e) => {
-            if e.channel_is_disconnected() {
-                io_threads.join()?;
-            }
-            return Err(e.into());
-        }
-    };
-    main_loop(connection, initialization_params)?;
-    io_threads.join()?;
-
-    // Shut down gracefully.
-    eprintln!("shutting down server");
-    Ok(())
-}
-
-fn main_loop(
-    connection: Connection,
-    params: serde_json::Value,
-) -> Result<(), Box<dyn Error + Sync + Send>> {
-    let _params: InitializeParams = serde_json::from_value(params).unwrap();
-    eprintln!("starting example main loop");
-    for msg in &connection.receiver {
-        eprintln!("got msg: {msg:?}");
-        match msg {
-            Message::Request(req) => {
-                if connection.handle_shutdown(&req)? {
-                    return Ok(());
-                }
-                eprintln!("got request: {req:?}");
-                match cast::<GotoDefinition>(req) {
-                    Ok((id, params)) => {
-                        eprintln!("got gotoDefinition request #{id}: {params:?}");
-                        let result = Some(GotoDefinitionResponse::Array(Vec::new()));
-                        let result = serde_json::to_value(&result).unwrap();
-                        let resp = Response { id, result: Some(result), error: None };
-                        connection.sender.send(Message::Response(resp))?;
-                        continue;
-                    }
-                    Err(err @ ExtractError::JsonError { .. }) => panic!("{err:?}"),
-                    Err(ExtractError::MethodMismatch(req)) => req,
-                };
-                // ...
-            }
-            Message::Response(resp) => {
-                eprintln!("got response: {resp:?}");
-            }
-            Message::Notification(not) => {
-                eprintln!("got notification: {not:?}");
-            }
-        }
-    }
-    Ok(())
-}
-
-fn cast<R>(req: Request) -> Result<(RequestId, R::Params), ExtractError<Request>>
-where
-    R: lsp_types::request::Request,
-    R::Params: serde::de::DeserializeOwned,
-{
-    req.extract(R::METHOD)
-}
diff --git a/src/tools/rust-analyzer/lib/lsp-server/examples/manual_test.sh b/src/tools/rust-analyzer/lib/lsp-server/examples/manual_test.sh
new file mode 100755
index 00000000000..d028ac43301
--- /dev/null
+++ b/src/tools/rust-analyzer/lib/lsp-server/examples/manual_test.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# Simple nine-packet LSP test for examples/minimal_lsp.rs
+# Usage (two tabs):
+#
+#   mkfifo /tmp/lsp_pipe          # one-time setup
+#   # tab 1 – run the server
+#   cat /tmp/lsp_pipe | cargo run --example minimal_lsp
+#
+#   # tab 2 – fire the packets (this script)
+#   bash examples/manual_test.sh          # blocks until server exits
+#
+# If you don’t use a second tab, run the script in the background:
+#
+#   bash examples/manual_test.sh &        # writer in background
+#   cat /tmp/lsp_pipe | cargo run --example minimal_lsp
+#
+# The script opens /tmp/lsp_pipe for writing (exec 3>) and sends each JSON
+# packet with a correct Content-Length header.
+#
+# One-liner alternative (single terminal, no FIFO):
+#
+#   cargo run --example minimal_lsp <<'EOF'
+#     … nine packets …
+#   EOF
+#
+# Both approaches feed identical bytes to minimal_lsp via stdin.
+
+set -eu
+PIPE=${1:-/tmp/lsp_pipe}
+
+mkfifo -m 600 "$PIPE" 2>/dev/null || true       # create once, ignore if exists
+
+# open write end so the fifo stays open
+exec 3> "$PIPE"
+
+send() {
+  local body=$1
+  local len=$(printf '%s' "$body" | wc -c)
+  printf 'Content-Length: %d\r\n\r\n%s' "$len" "$body" >&3
+}
+
+send '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}'
+send '{"jsonrpc":"2.0","method":"initialized","params":{}}'
+send '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///tmp/foo.rs","languageId":"rust","version":1,"text":"fn  main( ){println!(\"hi\") }"}}}'
+send '{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}}'
+send '{"jsonrpc":"2.0","id":3,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}}'
+send '{"jsonrpc":"2.0","id":4,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"position":{"line":0,"character":0}}}'
+send '{"jsonrpc":"2.0","id":5,"method":"textDocument/formatting","params":{"textDocument":{"uri":"file:///tmp/foo.rs"},"options":{"tabSize":4,"insertSpaces":true}}}'
+send '{"jsonrpc":"2.0","id":6,"method":"shutdown","params":null}'
+send '{"jsonrpc":"2.0","method":"exit","params":null}'
+
+exec 3>&-
+echo "Packets sent – watch the other terminal for responses."
diff --git a/src/tools/rust-analyzer/lib/lsp-server/examples/minimal_lsp.rs b/src/tools/rust-analyzer/lib/lsp-server/examples/minimal_lsp.rs
new file mode 100644
index 00000000000..5eef999e062
--- /dev/null
+++ b/src/tools/rust-analyzer/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, &note, &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(())
+}