mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-01 11:31:15 +00:00
Merge pull request #20233 from shashforge/lsp-minimal-example-clean
examples: add `minimal_lsp.rs` and FIFO test script
This commit is contained in:
commit
f73ce3c8a8
@ -16,6 +16,9 @@ crossbeam-channel.workspace = true
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lsp-types = "=0.95"
|
lsp-types = "=0.95"
|
||||||
ctrlc = "3.4.7"
|
ctrlc = "3.4.7"
|
||||||
|
anyhow.workspace = true
|
||||||
|
rustc-hash.workspace = true
|
||||||
|
toolchain.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
@ -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)
|
|
||||||
}
|
|
53
lib/lsp-server/examples/manual_test.sh
Executable file
53
lib/lsp-server/examples/manual_test.sh
Executable file
@ -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."
|
335
lib/lsp-server/examples/minimal_lsp.rs
Normal file
335
lib/lsp-server/examples/minimal_lsp.rs
Normal file
@ -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(())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user