Skip to main content

dada_lsp_server/
main.rs

1use std::str::FromStr;
2use std::sync::{Arc, Mutex};
3
4use dada_compiler::{Compiler, Fork, RealFs};
5use dada_ir_ast::diagnostic::{Diagnostic, DiagnosticLabel, Level};
6use dada_ir_ast::inputs::SourceFile;
7use dada_ir_ast::span::{AbsoluteOffset, AbsoluteSpan};
8use dada_util::{Fallible, Map, Set, bail};
9use lsp::{Editor, Lsp, LspFork};
10use lsp_types::{
11    DidChangeTextDocumentParams, DidOpenTextDocumentParams, HoverProviderCapability, MessageType,
12    OneOf, PublishDiagnosticsParams, TextDocumentContentChangeEvent, TextDocumentItem,
13    TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Uri,
14    VersionedTextDocumentIdentifier,
15};
16use lsp_types::{InitializeParams, ServerCapabilities};
17
18use salsa::{Database, Setter};
19use url::Url;
20
21mod lsp;
22
23fn main() -> Fallible<()> {
24    Server::run()
25}
26
27struct Server {
28    db: Compiler,
29    diagnostics: Arc<Mutex<EditorDiagnostics>>,
30}
31
32/// Tracks the diagnostics we have sent over to the editor.
33#[derive(Default)]
34struct EditorDiagnostics {
35    /// Track the source files for which we have published diagnostics to the editor.
36    has_published_diagnostics: Set<SourceFile>,
37}
38
39impl lsp::Lsp for Server {
40    type Fork = ServerFork;
41
42    fn new(_params: InitializeParams) -> Fallible<Self> {
43        Ok(Server {
44            db: Compiler::new(RealFs::new(), None),
45            diagnostics: Default::default(),
46        })
47    }
48
49    fn server_capabilities(&mut self) -> Fallible<ServerCapabilities> {
50        Ok(ServerCapabilities {
51            hover_provider: Some(HoverProviderCapability::Simple(true)),
52            text_document_sync: Some(TextDocumentSyncCapability::Options(
53                TextDocumentSyncOptions {
54                    open_close: Some(true),
55                    change: Some(TextDocumentSyncKind::FULL),
56                    will_save: None,
57                    will_save_wait_until: None,
58                    save: None,
59                },
60            )),
61            definition_provider: Some(OneOf::Left(true)),
62            ..ServerCapabilities::default()
63        })
64    }
65
66    fn server_info(&mut self) -> Fallible<Option<lsp_types::ServerInfo>> {
67        Ok(None)
68    }
69
70    fn fork(&mut self) -> Self::Fork {
71        ServerFork {
72            db: self.db.fork(),
73            diagnostics: self.diagnostics.clone(),
74        }
75    }
76
77    fn did_open(
78        &mut self,
79        editor: &mut dyn Editor<Self>,
80        params: DidOpenTextDocumentParams,
81    ) -> Fallible<()> {
82        let DidOpenTextDocumentParams { text_document } = params;
83        let TextDocumentItem {
84            uri,
85            language_id: _,
86            version: _,
87            text,
88        } = text_document;
89
90        let source_file = self.db.open_source_file(uri.as_str(), Ok(text))?;
91
92        editor.show_message(MessageType::INFO, format!("did open {}", uri.as_str()))?;
93
94        editor.spawn(ServerFork::check_all_task(source_file));
95
96        Ok(())
97    }
98
99    fn did_change(
100        &mut self,
101        editor: &mut dyn Editor<Self>,
102        params: DidChangeTextDocumentParams,
103    ) -> Fallible<()> {
104        let DidChangeTextDocumentParams {
105            text_document: VersionedTextDocumentIdentifier { uri, version: _ },
106            content_changes,
107        } = params;
108        let uri_str = uri.as_str();
109
110        let source_file = self.db.get_previously_opened_source_file(uri_str)?;
111
112        for TextDocumentContentChangeEvent {
113            range,
114            range_length: _,
115            text,
116        } in content_changes
117        {
118            match range {
119                Some(_) => {
120                    // FIXME: We should implement incremental change events; but LSP sends line/column
121                    // positions and that's just *annoying*.
122                    bail!("we requested full content change events");
123                }
124                None => {
125                    let _old_contents = source_file.set_contents(&mut self.db).to(Ok(text));
126                }
127            }
128        }
129
130        editor.show_message(MessageType::INFO, format!("did change {uri_str}"))?;
131
132        editor.spawn(ServerFork::check_all_task(source_file));
133
134        Ok(())
135    }
136
137    fn hover(
138        &mut self,
139        _editor: &mut dyn Editor<Self>,
140        params: lsp_types::HoverParams,
141    ) -> Fallible<Option<lsp_types::Hover>> {
142        let lsp_types::HoverParams {
143            text_document_position_params:
144                lsp_types::TextDocumentPositionParams {
145                    text_document: lsp_types::TextDocumentIdentifier { uri },
146                    position,
147                },
148            work_done_progress_params: _,
149        } = params;
150
151        // Get the source file
152        let source_file = self.db.get_previously_opened_source_file(uri.as_str())?;
153
154        // Convert LSP position to absolute offset
155        let line = position.line as usize;
156        let character = position.character as usize;
157
158        // Get line starts
159        let line_starts = source_file.line_starts(&self.db);
160
161        // Make sure the line is valid
162        if line >= line_starts.len() - 1 {
163            return Ok(None);
164        }
165
166        // Get the start offset of the line
167        let line_start = line_starts[line];
168
169        // Calculate the absolute offset
170        let offset = AbsoluteOffset::from(line_start.as_usize() + character);
171
172        // Create a span at the position
173        let span = AbsoluteSpan {
174            source_file,
175            start: offset,
176            end: offset,
177        };
178
179        // Use probe_expression_type to get the type
180        self.db.attach(|db| {
181            if let Some(type_str) = dada_probe::probe_expression_type(db, span) {
182                // Return hover response with the type
183                return Ok(Some(lsp_types::Hover {
184                    contents: lsp_types::HoverContents::Markup(lsp_types::MarkupContent {
185                        kind: lsp_types::MarkupKind::Markdown,
186                        value: format!("Type: `{type_str}`"),
187                    }),
188                    range: None,
189                }));
190            }
191
192            Ok(None)
193        })
194    }
195}
196
197struct ServerFork {
198    db: Fork<Compiler>,
199    diagnostics: Arc<Mutex<EditorDiagnostics>>,
200}
201
202impl LspFork for ServerFork {
203    fn fork(&self) -> Self {
204        ServerFork {
205            db: self.db.fork(),
206            diagnostics: self.diagnostics.clone(),
207        }
208    }
209}
210
211type CheckAllTask = Box<dyn FnOnce(&ServerFork, &mut dyn Editor<Server>) -> Fallible<()> + Send>;
212
213impl ServerFork {
214    fn check_all_task(source_file: SourceFile) -> CheckAllTask {
215        Box::new(move |this, editor| this.check_all(editor, source_file))
216    }
217
218    fn check_all(&self, editor: &mut dyn Editor<Server>, source_file: SourceFile) -> Fallible<()> {
219        let new_diagnostics = self.db.check_all(source_file);
220        self.diagnostics.lock().unwrap().reconcile_diagnostics(
221            &self.db,
222            editor,
223            new_diagnostics,
224        )?;
225        Ok(())
226    }
227}
228
229impl EditorDiagnostics {
230    fn reconcile_diagnostics(
231        &mut self,
232        db: &Compiler,
233        editor: &mut dyn Editor<Server>,
234        diagnostics: Vec<&Diagnostic>,
235    ) -> Fallible<()> {
236        let mut new_diagnostics: Map<SourceFile, Vec<Diagnostic>> = Map::default();
237
238        // Sort diagnostics by URI
239        for diagnostic in diagnostics {
240            new_diagnostics
241                .entry(diagnostic.span.source_file)
242                .or_default()
243                .push(diagnostic.clone());
244        }
245
246        // Publish new diagnostics for each URI that has them
247        for (&source_file, diagnostics) in &new_diagnostics {
248            editor.publish_diagnostics(PublishDiagnosticsParams {
249                uri: Self::lsp_uri(source_file.url(db)),
250                diagnostics: diagnostics
251                    .iter()
252                    .map(|d| Self::lsp_diagnostic(db, d))
253                    .collect(),
254                version: None,
255            })?;
256
257            // Record that we successfully published diagnostics for this source file
258            self.has_published_diagnostics.insert(source_file);
259        }
260
261        // Clear out diagnostics for URIs that no longer have any
262        let no_longer_have_diagnostics: Vec<SourceFile> = self
263            .has_published_diagnostics
264            .iter()
265            .filter(|source| !new_diagnostics.contains_key(source))
266            .copied()
267            .collect();
268        for source_file in no_longer_have_diagnostics {
269            editor.publish_diagnostics(PublishDiagnosticsParams {
270                uri: Self::lsp_uri(source_file.url(db)),
271                diagnostics: vec![],
272                version: None,
273            })?;
274            self.has_published_diagnostics.remove(&source_file);
275        }
276
277        Ok(())
278    }
279
280    fn lsp_diagnostic(db: &Compiler, diagnostic: &Diagnostic) -> lsp_types::Diagnostic {
281        let related_information = diagnostic
282            .labels
283            .iter()
284            .map(|label| Self::lsp_diagnostic_related_information(db, label))
285            .collect();
286
287        lsp_types::Diagnostic {
288            range: Self::lsp_range(db, diagnostic.span),
289            severity: Some(Self::lsp_severity(db, diagnostic.level)),
290            code: None,
291            code_description: None,
292            source: Some("Dada compiler".to_string()),
293            message: diagnostic.message.clone(),
294            related_information: Some(related_information),
295            tags: None,
296            data: None,
297        }
298    }
299
300    fn lsp_severity(_db: &Compiler, level: Level) -> lsp_types::DiagnosticSeverity {
301        match level {
302            Level::Note => lsp_types::DiagnosticSeverity::INFORMATION,
303            Level::Help => lsp_types::DiagnosticSeverity::HINT,
304            Level::Info => lsp_types::DiagnosticSeverity::INFORMATION,
305            Level::Warning => lsp_types::DiagnosticSeverity::WARNING,
306            Level::Error => lsp_types::DiagnosticSeverity::ERROR,
307        }
308    }
309
310    fn lsp_diagnostic_related_information(
311        db: &Compiler,
312        label: &DiagnosticLabel,
313    ) -> lsp_types::DiagnosticRelatedInformation {
314        let location = Self::lsp_location(db, label.span);
315        let message = label.message.clone();
316        lsp_types::DiagnosticRelatedInformation { location, message }
317    }
318
319    fn lsp_location(db: &Compiler, span: AbsoluteSpan) -> lsp_types::Location {
320        let uri = Self::lsp_uri(span.source_file.url(db));
321        let range = Self::lsp_range(db, span);
322        lsp_types::Location { uri, range }
323    }
324
325    fn lsp_range(db: &Compiler, span: AbsoluteSpan) -> lsp_types::Range {
326        let start = Self::lsp_position(db, span.source_file, span.start);
327        let end = Self::lsp_position(db, span.source_file, span.end);
328        lsp_types::Range { start, end }
329    }
330
331    fn lsp_position(
332        db: &Compiler,
333        source_file: SourceFile,
334        offset: AbsoluteOffset,
335    ) -> lsp_types::Position {
336        let (line, column) = source_file.line_col(db, offset);
337        lsp_types::Position {
338            line: line.as_u32(),
339            character: column.as_u32(),
340        }
341    }
342
343    fn lsp_uri(url: &Url) -> Uri {
344        Uri::from_str(url.as_str()).unwrap()
345    }
346}