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(¶ms.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 = ¶ms.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 = ¶ms.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 = ¶ms.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(¶ms.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 ¶ms.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}