Skip to main content

fml_lsp/
server.rs

1//! The language server: JSON-RPC messages in, responses and notifications out. It does no I/O,
2//! so any transport can drive it: standard input and output ([`crate::run_stdio`]), or a
3//! WebSocket in a web console.
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use fml_check::Definitions;
9use fml_syntax::{FormatError, FormatOptions, Severity};
10use lsp_types::{
11    CompletionOptions, CompletionParams, DiagnosticSeverity, DidChangeTextDocumentParams,
12    DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
13    DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse,
14    HoverParams, HoverProviderCapability, InitializeResult, Location, OneOf, Position,
15    PrepareRenameResponse, PublishDiagnosticsParams, ReferenceParams, RenameOptions, RenameParams,
16    SemanticTokensFullOptions, SemanticTokensOptions, SemanticTokensParams,
17    SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, SignatureHelpOptions,
18    SignatureHelpParams, TextDocumentPositionParams, TextDocumentSyncCapability,
19    TextDocumentSyncKind, TextEdit, Url, WorkDoneProgressOptions,
20};
21use serde::de::DeserializeOwned;
22use serde_json::{Value, json};
23
24use crate::document::Document;
25use crate::{completion, hover, navigate, semantic, signature, symbols};
26
27/// JSON-RPC error codes.
28const METHOD_NOT_FOUND: i64 = -32601;
29const INVALID_PARAMS: i64 = -32602;
30const REQUEST_FAILED: i64 = -32803;
31const SERVER_NOT_INITIALIZED: i64 = -32002;
32
33/// The definitions maps are checked against, shared with the host.
34pub type SharedDefinitions = Arc<dyn Definitions + Send + Sync>;
35
36/// A language server for FML documents.
37pub struct Server {
38    definitions: SharedDefinitions,
39    documents: HashMap<Url, Document>,
40    initialized: bool,
41    shutdown: bool,
42    exited: bool,
43}
44
45impl Server {
46    /// A server that checks maps against `definitions`.
47    pub fn new(definitions: SharedDefinitions) -> Self {
48        Self {
49            definitions,
50            documents: HashMap::new(),
51            initialized: false,
52            shutdown: false,
53            exited: false,
54        }
55    }
56
57    /// A server over the core definitions of a FHIR release.
58    #[cfg(feature = "fhirschema")]
59    pub fn with_release(release: fml_check::Release) -> Self {
60        Self::new(Arc::new(fml_check::CoreDefinitions::new(release)))
61    }
62
63    /// Replaces the definitions, as when packages are added, and checks the open documents
64    /// again. Returns their new diagnostics.
65    pub fn set_definitions(&mut self, definitions: SharedDefinitions) -> Vec<Value> {
66        self.definitions = definitions;
67        let uris: Vec<Url> = self.documents.keys().cloned().collect();
68        uris.into_iter()
69            .filter_map(|uri| {
70                let document = self.documents.remove(&uri)?;
71                let document =
72                    Document::new(document.text, document.version, self.definitions.as_ref());
73                let message = diagnostics(&uri, &document);
74                self.documents.insert(uri, document);
75                Some(message)
76            })
77            .collect()
78    }
79
80    /// Whether the client sent `exit`: the transport should close.
81    pub fn exited(&self) -> bool {
82        self.exited
83    }
84
85    /// The process exit code after `exit`: 0 when `shutdown` came first.
86    pub fn exit_code(&self) -> i32 {
87        if self.shutdown { 0 } else { 1 }
88    }
89
90    /// What the server can do.
91    pub fn capabilities() -> ServerCapabilities {
92        ServerCapabilities {
93            text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
94            hover_provider: Some(HoverProviderCapability::Simple(true)),
95            completion_provider: Some(CompletionOptions {
96                trigger_characters: Some(vec![".".into(), ":".into()]),
97                ..CompletionOptions::default()
98            }),
99            signature_help_provider: Some(SignatureHelpOptions {
100                trigger_characters: Some(vec!["(".into(), ",".into()]),
101                retrigger_characters: None,
102                work_done_progress_options: WorkDoneProgressOptions::default(),
103            }),
104            definition_provider: Some(OneOf::Left(true)),
105            references_provider: Some(OneOf::Left(true)),
106            rename_provider: Some(OneOf::Right(RenameOptions {
107                prepare_provider: Some(true),
108                work_done_progress_options: WorkDoneProgressOptions::default(),
109            })),
110            document_symbol_provider: Some(OneOf::Left(true)),
111            document_formatting_provider: Some(OneOf::Left(true)),
112            semantic_tokens_provider: Some(
113                SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
114                    legend: semantic::legend(),
115                    full: Some(SemanticTokensFullOptions::Bool(true)),
116                    range: None,
117                    work_done_progress_options: WorkDoneProgressOptions::default(),
118                }),
119            ),
120            ..ServerCapabilities::default()
121        }
122    }
123
124    /// Handles one message and returns what to send back: the response to a request, and
125    /// notifications such as diagnostics.
126    pub fn handle(&mut self, message: &Value) -> Vec<Value> {
127        let Some(method) = message.get("method").and_then(Value::as_str) else {
128            // A response to something the server sent; it sends no requests.
129            return Vec::new();
130        };
131        let params = message.get("params").cloned().unwrap_or(Value::Null);
132        match message.get("id").cloned() {
133            Some(id) => vec![self.request(id, method, params)],
134            None => self.notification(method, params),
135        }
136    }
137
138    fn request(&mut self, id: Value, method: &str, params: Value) -> Value {
139        if method == "initialize" {
140            self.initialized = true;
141            let result = InitializeResult {
142                capabilities: Self::capabilities(),
143                server_info: Some(ServerInfo {
144                    name: "fml-lsp".to_owned(),
145                    version: Some(env!("CARGO_PKG_VERSION").to_owned()),
146                }),
147            };
148            return response(id, json!(result));
149        }
150        if !self.initialized {
151            return error(id, SERVER_NOT_INITIALIZED, "the server is not initialized");
152        }
153        let result = match method {
154            "shutdown" => {
155                self.shutdown = true;
156                Ok(Value::Null)
157            }
158            "textDocument/hover" => self.with::<HoverParams>(params, |s, p| {
159                let (document, offset) = s.at(&p.text_document_position_params)?;
160                Some(json!(hover::hover(document, offset)?))
161            }),
162            "textDocument/completion" => self.with::<CompletionParams>(params, |s, p| {
163                let (document, offset) = s.at(&p.text_document_position)?;
164                let list = completion::completion(document, s.definitions.as_ref(), offset)?;
165                Some(json!(list))
166            }),
167            "textDocument/signatureHelp" => self.with::<SignatureHelpParams>(params, |s, p| {
168                let (document, offset) = s.at(&p.text_document_position_params)?;
169                Some(json!(signature::signature_help(document, offset)?))
170            }),
171            "textDocument/definition" => self.with::<GotoDefinitionParams>(params, |s, p| {
172                let position = &p.text_document_position_params;
173                let (document, offset) = s.at(position)?;
174                let range = navigate::definition(document, offset)?;
175                Some(json!(GotoDefinitionResponse::Scalar(Location::new(
176                    position.text_document.uri.clone(),
177                    document.range(range),
178                ))))
179            }),
180            "textDocument/references" => self.with::<ReferenceParams>(params, |s, p| {
181                let position = &p.text_document_position;
182                let (document, offset) = s.at(position)?;
183                let (symbol, _) = navigate::symbol_at(document, offset)?;
184                let mut ranges = navigate::occurrences(document, &symbol);
185                if !p.context.include_declaration && !ranges.is_empty() {
186                    ranges.remove(0);
187                }
188                let locations: Vec<Location> = ranges
189                    .into_iter()
190                    .map(|r| Location::new(position.text_document.uri.clone(), document.range(r)))
191                    .collect();
192                Some(json!(locations))
193            }),
194            "textDocument/prepareRename" => {
195                self.with::<TextDocumentPositionParams>(params, |s, p| {
196                    let (document, offset) = s.at(&p)?;
197                    let (_, range) = navigate::symbol_at(document, offset)?;
198                    Some(json!(PrepareRenameResponse::Range(document.range(range))))
199                })
200            }
201            "textDocument/rename" => match serde_json::from_value::<RenameParams>(params) {
202                Err(e) => Err((INVALID_PARAMS, e.to_string())),
203                Ok(p) => {
204                    let position = &p.text_document_position;
205                    match self.at(position) {
206                        None => Ok(Value::Null),
207                        Some((document, offset)) => navigate::rename(
208                            document,
209                            &position.text_document.uri,
210                            offset,
211                            &p.new_name,
212                        )
213                        .map(|edit| json!(edit))
214                        .map_err(|problem| (REQUEST_FAILED, problem)),
215                    }
216                }
217            },
218            "textDocument/documentSymbol" => self.with::<DocumentSymbolParams>(params, |s, p| {
219                let document = s.documents.get(&p.text_document.uri)?;
220                Some(json!(DocumentSymbolResponse::Nested(
221                    symbols::document_symbols(document)
222                )))
223            }),
224            "textDocument/formatting" => self.with::<DocumentFormattingParams>(params, |s, p| {
225                let document = s.documents.get(&p.text_document.uri)?;
226                let options = FormatOptions {
227                    indent_width: p.options.tab_size as usize,
228                    use_tabs: !p.options.insert_spaces,
229                };
230                match fml_syntax::format(&document.text, &options) {
231                    Ok(formatted) if formatted == document.text => Some(json!([])),
232                    Ok(formatted) => {
233                        let end = document.position(fml_syntax::TextSize::of(&document.text));
234                        let range = lsp_types::Range::new(Position::new(0, 0), end);
235                        Some(json!([TextEdit::new(range, formatted)]))
236                    }
237                    // Nothing to format until the syntax errors are fixed.
238                    Err(FormatError::Syntax(_) | FormatError::Unstable(_)) => None,
239                }
240            }),
241            "textDocument/semanticTokens/full" => {
242                self.with::<SemanticTokensParams>(params, |s, p| {
243                    let document = s.documents.get(&p.text_document.uri)?;
244                    Some(json!(semantic::semantic_tokens(document)))
245                })
246            }
247            _ => Err((METHOD_NOT_FOUND, format!("unknown method `{method}`"))),
248        };
249        match result {
250            Ok(value) => response(id, value),
251            Err((code, message)) => error(id, code, &message),
252        }
253    }
254
255    /// Parses the parameters and runs `f`; a `None` result is a `null` response.
256    fn with<P: DeserializeOwned>(
257        &mut self,
258        params: Value,
259        f: impl FnOnce(&Self, P) -> Option<Value>,
260    ) -> Result<Value, (i64, String)> {
261        let params: P =
262            serde_json::from_value(params).map_err(|e| (INVALID_PARAMS, e.to_string()))?;
263        Ok(f(self, params).unwrap_or(Value::Null))
264    }
265
266    fn at(
267        &self,
268        position: &TextDocumentPositionParams,
269    ) -> Option<(&Document, fml_syntax::TextSize)> {
270        let document = self.documents.get(&position.text_document.uri)?;
271        Some((document, document.offset(position.position)))
272    }
273
274    fn notification(&mut self, method: &str, params: Value) -> Vec<Value> {
275        match method {
276            "exit" => {
277                self.exited = true;
278                Vec::new()
279            }
280            "textDocument/didOpen" => {
281                let Ok(p) = serde_json::from_value::<DidOpenTextDocumentParams>(params) else {
282                    return Vec::new();
283                };
284                self.open(
285                    p.text_document.uri,
286                    p.text_document.text,
287                    p.text_document.version,
288                )
289            }
290            "textDocument/didChange" => {
291                let Ok(p) = serde_json::from_value::<DidChangeTextDocumentParams>(params) else {
292                    return Vec::new();
293                };
294                // Full synchronisation: the last change is the whole text.
295                let Some(change) = p.content_changes.into_iter().last() else {
296                    return Vec::new();
297                };
298                self.open(p.text_document.uri, change.text, p.text_document.version)
299            }
300            "textDocument/didClose" => {
301                let Ok(p) = serde_json::from_value::<DidCloseTextDocumentParams>(params) else {
302                    return Vec::new();
303                };
304                self.documents.remove(&p.text_document.uri);
305                vec![notification(
306                    "textDocument/publishDiagnostics",
307                    json!(PublishDiagnosticsParams::new(
308                        p.text_document.uri,
309                        Vec::new(),
310                        None
311                    )),
312                )]
313            }
314            _ => Vec::new(),
315        }
316    }
317
318    fn open(&mut self, uri: Url, text: String, version: i32) -> Vec<Value> {
319        let document = Document::new(text, version, self.definitions.as_ref());
320        let message = diagnostics(&uri, &document);
321        self.documents.insert(uri, document);
322        vec![message]
323    }
324}
325
326fn diagnostics(uri: &Url, document: &Document) -> Value {
327    let diagnostics = document
328        .diagnostics
329        .iter()
330        .map(|d| lsp_types::Diagnostic {
331            range: document.range(d.range),
332            severity: Some(match d.severity {
333                Severity::Error => DiagnosticSeverity::ERROR,
334                Severity::Warning => DiagnosticSeverity::WARNING,
335            }),
336            source: Some("fml".to_owned()),
337            message: d.message.clone(),
338            ..lsp_types::Diagnostic::default()
339        })
340        .collect();
341    notification(
342        "textDocument/publishDiagnostics",
343        json!(PublishDiagnosticsParams::new(
344            uri.clone(),
345            diagnostics,
346            Some(document.version)
347        )),
348    )
349}
350
351fn response(id: Value, result: Value) -> Value {
352    json!({ "jsonrpc": "2.0", "id": id, "result": result })
353}
354
355fn error(id: Value, code: i64, message: &str) -> Value {
356    json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
357}
358
359fn notification(method: &str, params: Value) -> Value {
360    json!({ "jsonrpc": "2.0", "method": method, "params": params })
361}