Skip to main content

increparse_lsp/
server.rs

1//! The `serve()` skeleton: implement one trait, get a complete server loop.
2//!
3//! Everything protocol-shaped lives here — the `initialize` handshake,
4//! capability advertisement, document bookkeeping, change translation,
5//! diagnostics publishing, `documentSymbol` dispatch, and the
6//! [`Connection`]-by-value discipline that makes shutdown unable to
7//! deadlock. Users describe *what* to parse and *how it fails*; this module
8//! owns *how the server behaves*.
9
10use std::collections::HashMap;
11use std::error::Error;
12
13use increparse::{CancelToken, Engine};
14use lsp_server::Connection;
15use lsp_types::{
16    CodeActionParams, CompletionParams, CompletionResponse, DidChangeTextDocumentParams,
17    DidCloseTextDocumentParams, DidOpenTextDocumentParams, HoverParams, Location, OneOf,
18    TextDocumentSyncCapability, TextDocumentSyncKind,
19};
20
21use crate::diagnostics::{self, DiagnosticsOptions, FailedNode};
22use crate::document::Document;
23use crate::encoding::PositionEncoding;
24
25/// The document store behind a running server: URI -> parsed document.
26#[allow(clippy::mutable_key_type)]
27pub type Documents<C> = HashMap<lsp_types::Uri, Document<C>>;
28
29/// The language-specific half of a server.
30///
31/// Implement this and hand it to [`serve`] (stdio) or [`serve_on`] (any
32/// transport). Everything else — lifecycle, change bookkeeping, diagnostics
33/// publishing, symbol dispatch — is the skeleton's job.///
34/// # Examples
35///
36/// A minimal server that parses files and reports nothing:
37///
38/// ```
39/// use increparse::{Engine, Outcome, Pass, Schedule, Span};
40/// use increparse_lsp::{Document, FailedNode, Language, serve};
41/// use lsp_types::Diagnostic;
42///
43/// #[derive(Clone, Debug, PartialEq, Eq)]
44/// enum Ctx {
45///     File,
46/// }
47///
48/// struct Accept;
49/// impl Pass for Accept {
50///     type Ctx = Ctx;
51///     fn parse(&self, _source: &str, _span: Span, _ctx: &Ctx) -> Outcome<Ctx> {
52///         Outcome::Done
53///     }
54/// }
55///
56/// struct MyLang {
57///     engine: Engine<Ctx>,
58/// }
59///
60/// impl MyLang {
61///     fn new() -> Self {
62///         let mut schedule = Schedule::new();
63///         schedule.push(Accept);
64///         Self {
65///             engine: Engine::new(schedule),
66///         }
67///     }
68/// }
69///
70/// impl Language<Ctx> for MyLang {
71///     fn engine(&self) -> &Engine<Ctx> {
72///         &self.engine
73///     }
74///
75///     fn root_ctx(&self) -> Ctx {
76///         Ctx::File
77///     }
78///
79///     fn diagnostic(&self, _doc: &Document<Ctx>, _node: FailedNode<'_, Ctx>) -> Option<Diagnostic> {
80///         None
81///     }
82/// }
83///
84/// # fn untouched() {
85/// // serve(MyLang::new())?;
86/// # }
87/// ```
88pub trait Language<C: Clone + PartialEq + Send + 'static>: Send + Sync + 'static {
89    /// Whether to advertise and answer `textDocument/documentSymbol`.
90    /// Defaults to `false`; flip to `true` and override [`symbols`](Self::symbols).
91    ///
92    /// For per-instance decisions (e.g. capability discovered from a config
93    /// file), override [`supports_symbols`](Self::supports_symbols) instead —
94    /// it defaults to this constant.
95    const SUPPORTS_SYMBOLS: bool = false;
96
97    /// Runtime hook for symbol support; defaults to
98    /// [`SUPPORTS_SYMBOLS`](Self::SUPPORTS_SYMBOLS).
99    fn supports_symbols(&self) -> bool {
100        Self::SUPPORTS_SYMBOLS
101    }
102
103    /// The pass schedule run over every document.
104    fn engine(&self) -> &Engine<C>;
105
106    /// Context for a freshly opened document's root region.
107    fn root_ctx(&self) -> C;
108
109    /// The position encoding to negotiate with the client.
110    /// Defaults to UTF-16, the LSP default and what most clients use.
111    fn encoding(&self) -> PositionEncoding {
112        PositionEncoding::Utf16
113    }
114
115    /// Renders the diagnostic for a failing region, or `None` to stay
116    /// silent (e.g. for contexts whose failure is expected).
117    ///
118    /// `doc` gives access to the document's text, URI, and version; the
119    /// returned diagnostic's range may be left empty, in which case it is
120    /// filled in from the node's span.
121    fn diagnostic(
122        &self,
123        doc: &Document<C>,
124        node: FailedNode<'_, C>,
125    ) -> Option<lsp_types::Diagnostic>;
126
127    /// Diagnostics that do not come from parse failures — lint-rule
128    /// violations, style warnings, anything computed from the settled
129    /// document rather than a failing tree node. Merged into the same
130    /// `publishDiagnostics` notification, after the parse diagnostics.
131    fn extra_diagnostics(&self, doc: &Document<C>) -> Vec<lsp_types::Diagnostic> {
132        let _ = doc;
133        Vec::new()
134    }
135
136    /// The document symbols for the outline view. Only consulted when
137    /// [`supports_symbols`](Self::supports_symbols) is `true`.
138    fn symbols(&self, doc: &Document<C>) -> Vec<lsp_types::DocumentSymbol> {
139        let _ = doc;
140        Vec::new()
141    }
142
143    /// Runtime hook for hover support; defaults to `false`.
144    fn supports_hover(&self) -> bool {
145        false
146    }
147
148    /// Runtime hook for go-to-definition support; defaults to `false`.
149    fn supports_definition(&self) -> bool {
150        false
151    }
152
153    /// Runtime hook for completion support; defaults to `false`.
154    fn supports_completion(&self) -> bool {
155        false
156    }
157
158    /// Runtime hook for code-action support (the editor's quickfix
159    /// lightbulb); defaults to `false`.
160    fn supports_code_actions(&self) -> bool {
161        false
162    }
163
164    /// Code actions for the given range — typically quickfixes for the
165    /// diagnostics published there. The skeleton dispatches
166    /// `textDocument/codeAction` when
167    /// [`supports_code_actions`](Self::supports_code_actions) is `true`.
168    fn code_action(
169        &self,
170        doc: &Document<C>,
171        range: lsp_types::Range,
172    ) -> Vec<lsp_types::CodeAction> {
173        let _ = (doc, range);
174        Vec::new()
175    }
176
177    /// Hover contents for the byte `offset` in `doc`, or `None`.
178    ///
179    /// The skeleton converts the client's position (in the negotiated
180    /// encoding) to a byte offset before calling this, so implementations
181    /// never touch position math.
182    fn hover(&self, doc: &Document<C>, offset: usize) -> Option<lsp_types::Hover> {
183        let _ = (doc, offset);
184        None
185    }
186
187    /// Definition locations for the byte `offset` in `doc`, or `None`.
188    fn definition(&self, doc: &Document<C>, offset: usize) -> Option<Vec<Location>> {
189        let _ = (doc, offset);
190        None
191    }
192
193    /// Completions for the byte `offset` in `doc`, or `None`.
194    fn completion(&self, doc: &Document<C>, offset: usize) -> Option<CompletionResponse> {
195        let _ = (doc, offset);
196        None
197    }
198}
199
200/// The server capabilities advertised for `language`.
201pub(crate) fn capabilities<C, L>(language: &L) -> lsp_types::ServerCapabilities
202where
203    C: Clone + PartialEq + Send + 'static,
204    L: Language<C>,
205{
206    lsp_types::ServerCapabilities {
207        position_encoding: Some(language.encoding().capability()),
208        text_document_sync: Some(TextDocumentSyncCapability::Kind(
209            TextDocumentSyncKind::INCREMENTAL,
210        )),
211        document_symbol_provider: Some(OneOf::Left(language.supports_symbols())),
212        hover_provider: Some(lsp_types::HoverProviderCapability::Simple(
213            language.supports_hover(),
214        )),
215        definition_provider: Some(OneOf::Left(language.supports_definition())),
216        completion_provider: language
217            .supports_completion()
218            .then(|| lsp_types::CompletionOptions {
219                ..Default::default()
220            }),
221        code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(
222            language.supports_code_actions(),
223        )),
224        ..Default::default()
225    }
226}
227
228fn publish<C, L>(
229    connection: &Connection,
230    language: &L,
231    doc: &Document<C>,
232) -> Result<(), Box<dyn Error + Send + Sync>>
233where
234    C: Clone + PartialEq + Send + 'static,
235    L: Language<C>,
236{
237    let mut diags = diagnostics::diagnostics(doc, DiagnosticsOptions::default(), |node| {
238        language.diagnostic(doc, node)
239    });
240    diags.extend(language.extra_diagnostics(doc));
241    let params = lsp_types::PublishDiagnosticsParams {
242        uri: doc.uri().clone(),
243        diagnostics: diags,
244        version: Some(doc.version()),
245    };
246    connection.sender.send(lsp_server::Message::Notification(
247        lsp_server::Notification::new("textDocument/publishDiagnostics".into(), params),
248    ))?;
249    Ok(())
250}
251
252/// Runs a full server lifecycle over an existing `connection`:
253/// the `initialize` handshake, then the message loop until `exit`.
254///
255/// Use this instead of [`serve`] when you own the transport (TCP, an
256/// in-process [`Connection::memory`] pair, tests). The caller is responsible
257/// for any I/O threads; `serve_on` returns once the client sends `shutdown`
258/// + `exit`.
259#[allow(clippy::mutable_key_type)]
260pub fn serve_on<C, L>(
261    connection: Connection,
262    language: L,
263    documents: Documents<C>,
264) -> Result<(), Box<dyn Error + Send + Sync>>
265where
266    C: Clone + PartialEq + Send + 'static,
267    L: Language<C>,
268{
269    #[allow(clippy::mutable_key_type)]
270    let mut documents = documents;
271    let _initialization_params =
272        connection.initialize(serde_json::to_value(capabilities(&language))?)?;
273
274    run_loop(connection, language, &mut documents)
275}
276
277#[allow(clippy::mutable_key_type)]
278fn run_loop<C, L>(
279    connection: Connection,
280    language: L,
281    documents: &mut Documents<C>,
282) -> Result<(), Box<dyn Error + Send + Sync>>
283where
284    C: Clone + PartialEq + Send + 'static,
285    L: Language<C>,
286{
287    for msg in &connection.receiver {
288        match msg {
289            lsp_server::Message::Request(req) => {
290                if connection.handle_shutdown(&req)? {
291                    break;
292                }
293                match req.method.as_str() {
294                    "textDocument/documentSymbol" if language.supports_symbols() => {
295                        let params: lsp_types::DocumentSymbolParams =
296                            serde_json::from_value(req.params)?;
297                        let symbols = documents
298                            .get(&params.text_document.uri)
299                            .map(|doc| language.symbols(doc))
300                            .unwrap_or_default();
301                        connection.sender.send(lsp_server::Message::Response(
302                            lsp_server::Response::new_ok(req.id, symbols),
303                        ))?;
304                    }
305                    "textDocument/hover" if language.supports_hover() => {
306                        let params: HoverParams = serde_json::from_value(req.params)?;
307                        let tdp = &params.text_document_position_params;
308                        let hover = documents
309                            .get(&tdp.text_document.uri)
310                            .and_then(|doc| language.hover(doc, doc.offset(tdp.position)));
311                        connection.sender.send(lsp_server::Message::Response(
312                            lsp_server::Response::new_ok(req.id, hover),
313                        ))?;
314                    }
315                    "textDocument/definition" if language.supports_definition() => {
316                        let params: lsp_types::GotoDefinitionParams =
317                            serde_json::from_value(req.params)?;
318                        let tdp = &params.text_document_position_params;
319                        let locations = documents
320                            .get(&tdp.text_document.uri)
321                            .and_then(|doc| language.definition(doc, doc.offset(tdp.position)));
322                        connection.sender.send(lsp_server::Message::Response(
323                            lsp_server::Response::new_ok(req.id, locations),
324                        ))?;
325                    }
326                    "textDocument/completion" if language.supports_completion() => {
327                        let params: CompletionParams = serde_json::from_value(req.params)?;
328                        let tdp = &params.text_document_position;
329                        let completions = documents
330                            .get(&tdp.text_document.uri)
331                            .and_then(|doc| language.completion(doc, doc.offset(tdp.position)));
332                        connection.sender.send(lsp_server::Message::Response(
333                            lsp_server::Response::new_ok(req.id, completions),
334                        ))?;
335                    }
336                    "textDocument/codeAction" if language.supports_code_actions() => {
337                        let params: CodeActionParams = serde_json::from_value(req.params)?;
338                        let actions = documents
339                            .get(&params.text_document.uri)
340                            .map(|doc| language.code_action(doc, params.range))
341                            .unwrap_or_default();
342                        connection.sender.send(lsp_server::Message::Response(
343                            lsp_server::Response::new_ok(
344                                req.id,
345                                actions
346                                    .into_iter()
347                                    .map(lsp_types::CodeActionOrCommand::CodeAction)
348                                    .collect::<Vec<_>>(),
349                            ),
350                        ))?;
351                    }
352                    _ => {
353                        connection.sender.send(lsp_server::Message::Response(
354                            lsp_server::Response::new_err(
355                                req.id,
356                                lsp_server::ErrorCode::MethodNotFound as i32,
357                                "method not supported".into(),
358                            ),
359                        ))?;
360                    }
361                }
362            }
363            lsp_server::Message::Notification(notification) => {
364                let lsp_server::Notification { method, params, .. } = notification;
365                match method.as_str() {
366                    "textDocument/didOpen" => {
367                        let params: DidOpenTextDocumentParams = serde_json::from_value(params)?;
368                        let item = params.text_document;
369                        let mut doc = Document::open(
370                            item.uri.clone(),
371                            item.version,
372                            item.language_id.clone(),
373                            item.text,
374                            language.encoding(),
375                            language.root_ctx(),
376                        );
377                        doc.apply_changes(
378                            language.engine(),
379                            item.version,
380                            &[],
381                            &increparse::SerialExecutor,
382                            &CancelToken::new(),
383                        );
384                        publish(&connection, &language, &doc)?;
385                        documents.insert(item.uri, doc);
386                    }
387                    "textDocument/didChange" => {
388                        let params: DidChangeTextDocumentParams = serde_json::from_value(params)?;
389                        let uri = params.text_document.uri.clone();
390                        if let Some(doc) = documents.get_mut(&uri) {
391                            doc.apply_changes(
392                                language.engine(),
393                                params.text_document.version,
394                                &params.content_changes,
395                                &increparse::SerialExecutor,
396                                &CancelToken::new(),
397                            );
398                            publish(&connection, &language, doc)?;
399                        }
400                    }
401                    "textDocument/didClose" => {
402                        let params: DidCloseTextDocumentParams = serde_json::from_value(params)?;
403                        let uri = params.text_document.uri;
404                        documents.remove(&uri);
405                        connection.sender.send(lsp_server::Message::Notification(
406                            lsp_server::Notification::new(
407                                "textDocument/publishDiagnostics".into(),
408                                lsp_types::PublishDiagnosticsParams {
409                                    uri,
410                                    diagnostics: Vec::new(),
411                                    version: None,
412                                },
413                            ),
414                        ))?;
415                    }
416                    _ => {}
417                }
418            }
419            lsp_server::Message::Response(_) => {}
420        }
421    }
422
423    Ok(())
424}
425
426/// Runs a language server on stdio until the client sends `shutdown` +
427/// `exit`.
428///
429/// This is the whole integration point:
430///
431/// ```no_run
432/// # use increparse::{Engine, Outcome, Pass, Schedule, Span};
433/// # use increparse_lsp::{Document, FailedNode, Language};
434/// # use lsp_types::Diagnostic;
435/// # #[derive(Clone, Debug, PartialEq, Eq)]
436/// # enum Ctx { File }
437/// # struct Accept;
438/// # impl Pass for Accept {
439/// #     type Ctx = Ctx;
440/// #     fn parse(&self, _source: &str, _span: Span, _ctx: &Ctx) -> Outcome<Ctx> {
441/// #         Outcome::Done
442/// #     }
443/// # }
444/// # struct MyLang { engine: Engine<Ctx> }
445/// # impl Language<Ctx> for MyLang {
446/// #     fn engine(&self) -> &Engine<Ctx> { &self.engine }
447/// #     fn root_ctx(&self) -> Ctx { Ctx::File }
448/// #     fn diagnostic(&self, _doc: &Document<Ctx>, _node: FailedNode<'_, Ctx>) -> Option<Diagnostic> { None }
449/// # }
450/// # fn build_engine() -> Engine<Ctx> { unimplemented!() }
451/// fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
452///     increparse_lsp::serve(MyLang { engine: build_engine() })
453/// }
454/// ```
455pub fn serve<C, L>(language: L) -> Result<(), Box<dyn Error + Send + Sync>>
456where
457    C: Clone + PartialEq + Send + 'static,
458    L: Language<C>,
459{
460    let (connection, io_threads) = Connection::stdio();
461    serve_on(connection, language, Documents::new())?;
462    io_threads.join()?;
463    Ok(())
464}