datex_core/lsp/
mod.rs

1mod errors;
2mod type_hint_collector;
3mod utils;
4mod variable_declaration_finder;
5use crate::ast::expressions::{
6    DatexExpressionData, VariableAccess, VariableAssignment,
7    VariableDeclaration,
8};
9use crate::collections::HashMap;
10use crate::compiler::precompiler::precompiled_ast::RichAst;
11use crate::compiler::workspace::CompilerWorkspace;
12use crate::lsp::errors::SpannedLSPCompilerError;
13use crate::lsp::variable_declaration_finder::VariableDeclarationFinder;
14use crate::runtime::Runtime;
15use crate::stdlib::borrow::Cow;
16use crate::stdlib::cell::RefCell;
17use crate::values::core_values::r#type::Type;
18use crate::visitor::expression::ExpressionVisitor;
19use realhydroper_lsp::jsonrpc::{Error, ErrorCode};
20use realhydroper_lsp::{Client, LanguageServer, Server};
21use realhydroper_lsp::{LspService, lsp_types::*};
22
23#[cfg(feature = "lsp_wasm")]
24use futures::io::{AsyncRead, AsyncWrite};
25#[cfg(not(feature = "lsp_wasm"))]
26use tokio::io::{AsyncRead, AsyncWrite};
27
28pub struct LanguageServerBackend {
29    pub client: Client,
30    pub compiler_workspace: RefCell<CompilerWorkspace>,
31    pub spanned_compiler_errors:
32        RefCell<HashMap<Url, Vec<SpannedLSPCompilerError>>>,
33}
34
35impl LanguageServerBackend {
36    pub fn new(client: Client, compiler_workspace: CompilerWorkspace) -> Self {
37        Self {
38            client,
39            compiler_workspace: RefCell::new(compiler_workspace),
40            spanned_compiler_errors: RefCell::new(HashMap::new()),
41        }
42    }
43}
44
45#[realhydroper_lsp::async_trait(?Send)]
46impl LanguageServer for LanguageServerBackend {
47    async fn initialize(
48        &self,
49        _: InitializeParams,
50    ) -> realhydroper_lsp::jsonrpc::Result<InitializeResult> {
51        Ok(InitializeResult {
52            capabilities: ServerCapabilities {
53                hover_provider: Some(HoverProviderCapability::Simple(true)),
54                completion_provider: Some(CompletionOptions::default()),
55                text_document_sync: Some(TextDocumentSyncCapability::Kind(
56                    TextDocumentSyncKind::FULL,
57                )),
58                diagnostic_provider: Some(
59                    DiagnosticServerCapabilities::Options(DiagnosticOptions {
60                        inter_file_dependencies: true,
61                        workspace_diagnostics: false,
62                        identifier: None,
63                        work_done_progress_options: WorkDoneProgressOptions {
64                            work_done_progress: None,
65                        },
66                    }),
67                ),
68                inlay_hint_provider: Some(OneOf::Left(true)),
69                document_link_provider: Some(DocumentLinkOptions {
70                    resolve_provider: Some(true),
71                    work_done_progress_options: Default::default(),
72                }),
73                definition_provider: Some(OneOf::Left(true)),
74                ..Default::default()
75            },
76            ..Default::default()
77        })
78    }
79
80    async fn initialized(&self, _: InitializedParams) {
81        self.client
82            .log_message(MessageType::INFO, "server initialized!")
83            .await;
84    }
85
86    async fn shutdown(&self) -> realhydroper_lsp::jsonrpc::Result<()> {
87        Ok(())
88    }
89
90    async fn did_open(&self, params: DidOpenTextDocumentParams) {
91        self.client
92            .log_message(
93                MessageType::INFO,
94                format!("File opened: {}", params.text_document.uri),
95            )
96            .await;
97
98        self.update_file_contents(
99            params.text_document.uri,
100            params.text_document.text,
101        )
102        .await;
103    }
104
105    async fn did_change(&self, params: DidChangeTextDocumentParams) {
106        self.client
107            .log_message(
108                MessageType::INFO,
109                format!("File changed: {}", params.text_document.uri),
110            )
111            .await;
112        let new_content = params
113            .content_changes
114            .into_iter()
115            .next()
116            .map(|change| change.text)
117            .unwrap_or_default();
118        self.update_file_contents(params.text_document.uri, new_content)
119            .await;
120    }
121
122    async fn completion(
123        &self,
124        params: CompletionParams,
125    ) -> realhydroper_lsp::jsonrpc::Result<Option<CompletionResponse>> {
126        self.client
127            .log_message(MessageType::INFO, "completion!")
128            .await;
129
130        let position = params.text_document_position;
131
132        // For simplicity, we assume the prefix is the last word before the cursor.
133        // In a real implementation, you would extract this from the document content.
134        let prefix = self.get_previous_text_at_position(&position);
135        self.client
136            .log_message(
137                MessageType::INFO,
138                format!("Completion prefix: {}", prefix),
139            )
140            .await;
141
142        let variables = self.find_variable_starting_with(&prefix);
143
144        let items: Vec<CompletionItem> = variables
145            .iter()
146            .map(|var| CompletionItem {
147                label: var.name.clone(),
148                kind: Some(CompletionItemKind::VARIABLE),
149                detail: Some(format!(
150                    "{} {}: {}",
151                    var.shape,
152                    var.name,
153                    var.var_type.as_ref().unwrap()
154                )),
155                documentation: None,
156                ..Default::default()
157            })
158            .collect();
159
160        Ok(Some(CompletionResponse::Array(items)))
161    }
162
163    async fn hover(
164        &self,
165        params: HoverParams,
166    ) -> realhydroper_lsp::jsonrpc::Result<Option<Hover>> {
167        let expression = self
168            .get_expression_at_position(&params.text_document_position_params);
169
170        if let Some(expression) = expression {
171            Ok(match expression.data {
172                // show variable type info on hover
173                DatexExpressionData::VariableDeclaration(
174                    VariableDeclaration {
175                        name, id: Some(id), ..
176                    },
177                )
178                | DatexExpressionData::VariableAssignment(
179                    VariableAssignment {
180                        name, id: Some(id), ..
181                    },
182                )
183                | DatexExpressionData::VariableAccess(VariableAccess {
184                    id,
185                    name,
186                }) => {
187                    let variable_metadata =
188                        self.get_variable_by_id(id).unwrap();
189                    Some(self.get_language_string_hover(&format!(
190                        "{} {}: {}",
191                        variable_metadata.shape,
192                        name,
193                        variable_metadata.var_type.unwrap_or(Type::unknown())
194                    )))
195                }
196
197                // show value info on hover for literals
198                DatexExpressionData::Integer(integer) => Some(
199                    self.get_language_string_hover(&format!("{}", integer)),
200                ),
201                DatexExpressionData::TypedInteger(typed_integer) => {
202                    Some(self.get_language_string_hover(&format!(
203                        "{}",
204                        typed_integer
205                    )))
206                }
207                DatexExpressionData::Decimal(decimal) => Some(
208                    self.get_language_string_hover(&format!("{}", decimal)),
209                ),
210                DatexExpressionData::TypedDecimal(typed_decimal) => {
211                    Some(self.get_language_string_hover(&format!(
212                        "{}",
213                        typed_decimal
214                    )))
215                }
216                DatexExpressionData::Boolean(boolean) => Some(
217                    self.get_language_string_hover(&format!("{}", boolean)),
218                ),
219                DatexExpressionData::Text(text) => Some(
220                    self.get_language_string_hover(&format!("\"{}\"", text)),
221                ),
222                DatexExpressionData::Endpoint(endpoint) => Some(
223                    self.get_language_string_hover(&format!("{}", endpoint)),
224                ),
225                DatexExpressionData::Null => {
226                    Some(self.get_language_string_hover("null"))
227                }
228
229                _ => None,
230            })
231        } else {
232            Err(realhydroper_lsp::jsonrpc::Error {
233                code: ErrorCode::ParseError,
234                message: Cow::from("No AST available"),
235                data: None,
236            })
237        }
238    }
239
240    async fn inlay_hint(
241        &self,
242        params: InlayHintParams,
243    ) -> realhydroper_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
244        // show type hints for variables
245        let type_hints = self
246            .get_type_hints(params.text_document.uri)
247            .unwrap()
248            .into_iter()
249            .map(|hint| InlayHint {
250                position: hint.0,
251                label: InlayHintLabel::String(format!(": {}", hint.1.unwrap())),
252                kind: Some(InlayHintKind::TYPE),
253                text_edits: None,
254                tooltip: None,
255                padding_left: Some(true),
256                padding_right: None,
257                data: None,
258            })
259            .collect();
260
261        Ok(Some(type_hints))
262    }
263
264    async fn goto_definition(
265        &self,
266        params: GotoDefinitionParams,
267    ) -> realhydroper_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
268        let expression = self
269            .get_expression_at_position(&params.text_document_position_params);
270        if let Some(expression) = expression {
271            match expression.data {
272                DatexExpressionData::VariableAccess(VariableAccess {
273                    id,
274                    name,
275                }) => {
276                    let uri =
277                        params.text_document_position_params.text_document.uri;
278                    let mut workspace = self.compiler_workspace.borrow_mut();
279                    let file = workspace.get_file_mut(&uri).unwrap();
280                    if let Some(RichAst { ast, .. }) = &mut file.rich_ast {
281                        let mut finder = VariableDeclarationFinder::new(id);
282                        finder.visit_datex_expression(ast);
283                        Ok(finder.variable_declaration_position.map(
284                            |position| {
285                                GotoDefinitionResponse::Scalar(Location {
286                                    uri,
287                                    range: self
288                                        .convert_byte_range_to_document_range(
289                                            &position,
290                                            &file.content,
291                                        ),
292                                })
293                            },
294                        ))
295                    } else {
296                        Ok(None)
297                    }
298                }
299                _ => Ok(None),
300            }
301        } else {
302            Err(Error::internal_error())
303        }
304    }
305
306    async fn document_link(
307        &self,
308        params: DocumentLinkParams,
309    ) -> realhydroper_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
310        // TODO #679
311        Ok(Some(vec![]))
312    }
313
314    // get error diagnostics
315    async fn diagnostic(
316        &self,
317        params: DocumentDiagnosticParams,
318    ) -> realhydroper_lsp::jsonrpc::Result<DocumentDiagnosticReportResult> {
319        self.client
320            .log_message(MessageType::INFO, "diagnostics!")
321            .await;
322
323        let uri = params.text_document.uri;
324        let diagnostics = self.get_diagnostics_for_file(&uri);
325        let report = FullDocumentDiagnosticReport {
326            result_id: None,
327            items: diagnostics,
328        };
329
330        Ok(DocumentDiagnosticReportResult::Report(
331            DocumentDiagnosticReport::Full(
332                RelatedFullDocumentDiagnosticReport {
333                    related_documents: None,
334                    full_document_diagnostic_report: report,
335                },
336            ),
337        ))
338    }
339}
340
341impl LanguageServerBackend {
342    fn get_language_string_hover(&self, text: &str) -> Hover {
343        let contents = HoverContents::Scalar(MarkedString::LanguageString(
344            LanguageString {
345                language: "datex".to_string(),
346                value: text.to_string(),
347            },
348        ));
349        Hover {
350            contents,
351            range: None,
352        }
353    }
354
355    fn get_diagnostics_for_file(&self, url: &Url) -> Vec<Diagnostic> {
356        let mut diagnostics = Vec::new();
357        let errors = self.spanned_compiler_errors.borrow();
358        if let Some(file_errors) = errors.get(url) {
359            for spanned_error in file_errors {
360                let diagnostic = Diagnostic {
361                    range: spanned_error.span,
362                    severity: Some(DiagnosticSeverity::ERROR),
363                    code: None,
364                    code_description: None,
365                    source: Some("datex".to_string()),
366                    message: format!("{}", spanned_error.error),
367                    related_information: None,
368                    tags: None,
369                    data: None,
370                };
371                diagnostics.push(diagnostic);
372            }
373        }
374        diagnostics
375    }
376}
377
378pub fn create_lsp<I, O>(
379    runtime: Runtime,
380    input: I,
381    output: O,
382) -> impl core::future::Future<Output = ()>
383where
384    I: AsyncRead + Unpin,
385    O: AsyncWrite,
386{
387    let compiler_workspace = CompilerWorkspace::new(runtime);
388    let (service, socket) = LspService::new(|client| {
389        LanguageServerBackend::new(client, compiler_workspace)
390    });
391    Server::new(input, output, socket).serve(service)
392}
393
394#[cfg(test)]
395mod tests {
396    use core::str::FromStr;
397
398    use crate::runtime::{AsyncContext, RuntimeConfig};
399    use crate::values::core_values::endpoint::Endpoint;
400
401    use super::*;
402    use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
403    use tokio::task::LocalSet;
404    use tokio::time::{Duration, timeout};
405
406    #[tokio::test(flavor = "current_thread")]
407    async fn test_lsp_initialization() {
408        // LocalSet is required for spawn_local
409        let local = LocalSet::new();
410
411        local
412            .run_until(async {
413                let runtime = Runtime::new(
414                    RuntimeConfig::new_with_endpoint(
415                        Endpoint::from_str("@lspler").unwrap(),
416                    ),
417                    AsyncContext::new(),
418                );
419
420                let (mut client_read, server_write) = duplex(1024);
421                let (server_read, mut client_write) = duplex(1024);
422
423                let lsp_future = create_lsp(runtime, server_read, server_write);
424                let lsp_handle = tokio::task::spawn_local(lsp_future);
425
426                // Send initialize request
427                let init_body = r#"{
428                    "jsonrpc": "2.0",
429                    "id": 1,
430                    "method": "initialize",
431                    "params": {
432                        "capabilities": {},
433                        "rootUri": null,
434                        "workspaceFolders": null
435                    }
436                }"#;
437
438                let init_request = format!(
439                    "Content-Length: {}\r\n\r\n{}",
440                    init_body.len(),
441                    init_body
442                );
443
444                client_write
445                    .write_all(init_request.as_bytes())
446                    .await
447                    .unwrap();
448
449                // Read response
450                let mut buffer = vec![0; 1024];
451                let n = timeout(
452                    Duration::from_secs(2),
453                    client_read.read(&mut buffer),
454                )
455                .await
456                .unwrap()
457                .unwrap();
458
459                let response = String::from_utf8_lossy(&buffer[..n]);
460                assert!(response.contains(r#""id":1"#));
461                lsp_handle.abort();
462            })
463            .await;
464    }
465}