datex_core/lsp/
utils.rs

1use crate::ast::expressions::{
2    DatexExpression, DatexExpressionData, List, Map, Statements,
3    VariableAccess, VariableAssignment, VariableDeclaration,
4};
5use crate::compiler::error::DetailedCompilerErrors;
6use crate::compiler::precompiler::precompiled_ast::VariableMetadata;
7use crate::lsp::LanguageServerBackend;
8use crate::lsp::errors::SpannedLSPCompilerError;
9use crate::lsp::type_hint_collector::TypeHintCollector;
10use crate::values::core_values::decimal::Decimal;
11use crate::values::core_values::decimal::typed_decimal::TypedDecimal;
12use crate::values::core_values::endpoint::Endpoint;
13use crate::values::core_values::integer::Integer;
14use crate::values::core_values::integer::typed_integer::TypedInteger;
15use crate::values::core_values::r#type::Type;
16use crate::visitor::VisitAction;
17use crate::visitor::expression::ExpressionVisitor;
18use crate::visitor::type_expression::TypeExpressionVisitor;
19use realhydroper_lsp::lsp_types::{
20    MessageType, Position, Range, TextDocumentPositionParams,
21};
22use url::Url;
23
24impl LanguageServerBackend {
25    pub async fn update_file_contents(&self, url: Url, content: String) {
26        let mut compiler_workspace = self.compiler_workspace.borrow_mut();
27        let file = compiler_workspace.load_file(url.clone(), content.clone());
28        // Clear previous errors for this file
29        self.clear_compiler_errors(&url);
30        if let Some(errors) = &file.errors {
31            self.client
32                .log_message(
33                    MessageType::ERROR,
34                    format!("Failed to compile file {}: {}", url, errors,),
35                )
36                .await;
37            self.collect_compiler_errors(errors, url, &content)
38        }
39        if let Some(rich_ast) = &file.rich_ast {
40            self.client
41                .log_message(
42                    MessageType::INFO,
43                    format!("AST: {:#?}", rich_ast.ast),
44                )
45                .await;
46            self.client
47                .log_message(
48                    MessageType::INFO,
49                    format!("AST metadata: {:#?}", *rich_ast.metadata.borrow()),
50                )
51                .await;
52        }
53    }
54
55    pub(crate) fn get_type_hints(
56        &self,
57        url: Url,
58    ) -> Option<Vec<(Position, Option<Type>)>> {
59        let mut workspace = self.compiler_workspace.borrow_mut();
60        let file = workspace.get_file_mut(&url).unwrap();
61        if let Some(rich_ast) = &mut file.rich_ast {
62            let ast = &mut rich_ast.ast;
63            let mut collector = TypeHintCollector::default();
64            collector.visit_datex_expression(ast);
65            Some(
66                collector
67                    .type_hints
68                    .into_iter()
69                    .map(|hint| {
70                        (
71                            self.byte_offset_to_position(hint.0, &file.content)
72                                .unwrap(),
73                            rich_ast
74                                .metadata
75                                .borrow()
76                                .variables
77                                .get(hint.1)
78                                .unwrap()
79                                .var_type
80                                .clone(),
81                        )
82                    })
83                    .collect(),
84            )
85        } else {
86            None
87        }
88    }
89
90    /// Clears all compiler errors associated with the given file URL.
91    fn clear_compiler_errors(&self, url: &Url) {
92        let mut spanned_compiler_errors =
93            self.spanned_compiler_errors.borrow_mut();
94        spanned_compiler_errors.remove(url);
95    }
96
97    /// Recursively collects spanned compiler errors into the spanned_compiler_errors field.
98    fn collect_compiler_errors(
99        &self,
100        errors: &DetailedCompilerErrors,
101        url: Url,
102        file_content: &String,
103    ) {
104        let mut spanned_compiler_errors =
105            self.spanned_compiler_errors.borrow_mut();
106        let file_errors =
107            spanned_compiler_errors.entry(url.clone()).or_default();
108
109        for error in &errors.errors {
110            let span = error
111                .span
112                .as_ref()
113                .map(|span| {
114                    self.convert_byte_range_to_document_range(
115                        span,
116                        file_content,
117                    )
118                })
119                .unwrap_or_else(|| {
120                    self.convert_byte_range_to_document_range(
121                        &(0..file_content.len()),
122                        file_content,
123                    )
124                });
125            file_errors.push(SpannedLSPCompilerError {
126                span,
127                error: error.error.clone(),
128            });
129        }
130    }
131
132    /// Finds all variables in the workspace whose names start with the given prefix.
133    pub fn find_variable_starting_with(
134        &self,
135        prefix: &str,
136    ) -> Vec<VariableMetadata> {
137        let compiler_workspace = self.compiler_workspace.borrow();
138        let mut results = Vec::new();
139        for file in compiler_workspace.files().values() {
140            if let Some(rich_ast) = &file.rich_ast {
141                let metadata = rich_ast.metadata.borrow();
142                for var in metadata.variables.iter() {
143                    if var.name.starts_with(prefix) {
144                        results.push(var.clone());
145                    }
146                }
147            }
148        }
149        results
150    }
151
152    /// Retrieves variable metadata by its unique ID.
153    pub fn get_variable_by_id(&self, id: usize) -> Option<VariableMetadata> {
154        let compiler_workspace = self.compiler_workspace.borrow();
155        for file in compiler_workspace.files().values() {
156            if let Some(rich_ast) = &file.rich_ast {
157                let metadata = rich_ast.metadata.borrow();
158                if let Some(v) = metadata.variables.get(id).cloned() {
159                    return Some(v);
160                }
161            }
162        }
163        None
164    }
165
166    /// Converts an LSP position (line and character) to a byte offset in the file content.
167    fn position_to_byte_offset(
168        &self,
169        position: &TextDocumentPositionParams,
170    ) -> usize {
171        let workspace = self.compiler_workspace.borrow();
172        // first get file contents at position.text_document.uri
173        // then calculate byte offset from position.position.line and position.position.character
174        let file_content = &workspace
175            .get_file(&position.text_document.uri)
176            .unwrap()
177            .content;
178
179        Self::line_char_to_byte_index(
180            file_content,
181            position.position.line as usize,
182            position.position.character as usize,
183        )
184        .unwrap_or(0)
185    }
186
187    /// Converts a byte range (start, end) to a document Range (start Position, end Position) in the file content.
188    pub fn convert_byte_range_to_document_range(
189        &self,
190        span: &core::ops::Range<usize>,
191        file_content: &String,
192    ) -> Range {
193        let start = self
194            .byte_offset_to_position(span.start, file_content)
195            .unwrap_or(Position {
196                line: 0,
197                character: 0,
198            });
199        let end = self
200            .byte_offset_to_position(span.end, file_content)
201            .unwrap_or(Position {
202                line: 0,
203                character: 0,
204            });
205        Range { start, end }
206    }
207
208    /// Converts a byte offset to an LSP position (line and character) in the file content.
209    /// TODO #678: check if this is correct, generated with copilot
210    pub fn byte_offset_to_position(
211        &self,
212        byte_offset: usize,
213        file_content: &String,
214    ) -> Option<Position> {
215        let mut current_offset = 0;
216        for (line_idx, line) in file_content.lines().enumerate() {
217            let line_length = line.len() + 1; // +1 for the newline character
218            if current_offset + line_length > byte_offset {
219                // The byte offset is within this line
220                let char_offset = line
221                    .char_indices()
222                    .find(|(i, _)| current_offset + i >= byte_offset)
223                    .map(|(i, _)| i)
224                    .unwrap_or(line.len());
225                return Some(Position {
226                    line: line_idx as u32,
227                    character: char_offset as u32,
228                });
229            }
230            current_offset += line_length;
231        }
232        None
233    }
234
235    /// Retrieves the text immediately preceding the given position in the document.
236    /// This is used for autocompletion suggestions.
237    pub fn get_previous_text_at_position(
238        &self,
239        position: &TextDocumentPositionParams,
240    ) -> String {
241        let byte_offset = self.position_to_byte_offset(position);
242        let workspace = self.compiler_workspace.borrow();
243        let file_content = &workspace
244            .get_file(&position.text_document.uri)
245            .unwrap()
246            .content;
247        // Get the text before the byte offset, only matching word characters
248        let previous_text = &file_content[..byte_offset];
249        let last_word = previous_text
250            .rsplit(|c: char| !c.is_alphanumeric() && c != '_')
251            .next()
252            .unwrap_or("");
253        last_word.to_string()
254    }
255
256    /// Retrieves the DatexExpression AST node at the given byte offset.
257    pub fn get_expression_at_position(
258        &self,
259        position: &TextDocumentPositionParams,
260    ) -> Option<DatexExpression> {
261        let byte_offset = self.position_to_byte_offset(position);
262        let mut workspace = self.compiler_workspace.borrow_mut();
263        if let Some(rich_ast) = &mut workspace
264            .get_file_mut(&position.text_document.uri)
265            .unwrap()
266            .rich_ast
267        {
268            let ast = &mut rich_ast.ast;
269            let mut finder = ExpressionFinder::new(byte_offset);
270            finder.visit_datex_expression(ast);
271            finder.found_expr.map(|e| DatexExpression {
272                span: e.1,
273                data: e.0,
274                ty: None,
275            })
276        } else {
277            None
278        }
279    }
280
281    /// Converts a (line, character) pair to a byte index in the given text.
282    /// Lines and characters are zero-indexed.
283    /// Returns None if the line or character is out of bounds.
284    pub fn line_char_to_byte_index(
285        text: &str,
286        line: usize,
287        character: usize,
288    ) -> Option<usize> {
289        let mut lines = text.split('\n');
290
291        // Get the line
292        let line_text = lines.nth(line)?;
293
294        // Compute byte index of the start of that line
295        let byte_offset_to_line_start = text
296            .lines()
297            .take(line)
298            .map(|l| l.len() + 1) // +1 for '\n'
299            .sum::<usize>();
300
301        // Now find the byte index within that line for the given character offset
302        let byte_offset_within_line = line_text
303            .char_indices()
304            .nth(character)
305            .map(|(i, _)| i)
306            .unwrap_or_else(|| line_text.len());
307
308        Some(byte_offset_to_line_start + byte_offset_within_line)
309    }
310}
311
312/// Visitor that finds the most specific DatexExpression containing a given byte position.
313/// If multiple expressions contain the position, the one with the smallest span is chosen.
314struct ExpressionFinder {
315    pub search_pos: usize,
316    pub found_expr: Option<(DatexExpressionData, core::ops::Range<usize>)>,
317}
318
319impl ExpressionFinder {
320    pub fn new(search_pos: usize) -> Self {
321        Self {
322            search_pos,
323            found_expr: None,
324        }
325    }
326
327    /// Checks if the given span includes the search position.
328    /// If it does, updates found_expr if this expression is more specific (smaller span).
329    /// Returns true if the span includes the search position, false otherwise.
330    fn match_span(
331        &mut self,
332        span: &core::ops::Range<usize>,
333        expr_data: DatexExpressionData,
334    ) -> Result<VisitAction<DatexExpression>, ()> {
335        if span.start <= self.search_pos && self.search_pos <= span.end {
336            // If we already found an expression, only replace it if this one is smaller (more specific)
337            if let Some((_, existing_expr_span)) = &self.found_expr {
338                if (span.end - span.start)
339                    < (existing_expr_span.end - existing_expr_span.start)
340                {
341                    self.found_expr = Some((expr_data, span.clone()));
342                }
343            } else {
344                self.found_expr = Some((expr_data, span.clone()));
345            }
346            Ok(VisitAction::VisitChildren)
347        } else {
348            Ok(VisitAction::SkipChildren)
349        }
350    }
351}
352
353impl TypeExpressionVisitor<()> for ExpressionFinder {}
354
355impl ExpressionVisitor<()> for ExpressionFinder {
356    fn visit_statements(
357        &mut self,
358        stmts: &mut Statements,
359        span: &core::ops::Range<usize>,
360    ) -> Result<VisitAction<DatexExpression>, ()> {
361        self.match_span(span, DatexExpressionData::Statements(stmts.clone()))
362    }
363
364    fn visit_variable_declaration(
365        &mut self,
366        var_decl: &mut VariableDeclaration,
367        span: &core::ops::Range<usize>,
368    ) -> Result<VisitAction<DatexExpression>, ()> {
369        self.match_span(
370            span,
371            DatexExpressionData::VariableDeclaration(var_decl.clone()),
372        )
373    }
374
375    fn visit_variable_assignment(
376        &mut self,
377        var_assign: &mut VariableAssignment,
378        span: &core::ops::Range<usize>,
379    ) -> Result<VisitAction<DatexExpression>, ()> {
380        self.match_span(
381            span,
382            DatexExpressionData::VariableAssignment(var_assign.clone()),
383        )
384    }
385
386    fn visit_variable_access(
387        &mut self,
388        var_access: &mut VariableAccess,
389        span: &core::ops::Range<usize>,
390    ) -> Result<VisitAction<DatexExpression>, ()> {
391        self.match_span(
392            span,
393            DatexExpressionData::VariableAccess(var_access.clone()),
394        )
395    }
396
397    fn visit_list(
398        &mut self,
399        list: &mut List,
400        span: &core::ops::Range<usize>,
401    ) -> Result<VisitAction<DatexExpression>, ()> {
402        self.match_span(span, DatexExpressionData::List(list.clone()))
403    }
404
405    fn visit_map(
406        &mut self,
407        map: &mut Map,
408        span: &core::ops::Range<usize>,
409    ) -> Result<VisitAction<DatexExpression>, ()> {
410        self.match_span(span, DatexExpressionData::Map(map.clone()))
411    }
412
413    fn visit_integer(
414        &mut self,
415        value: &mut Integer,
416        span: &core::ops::Range<usize>,
417    ) -> Result<VisitAction<DatexExpression>, ()> {
418        self.match_span(span, DatexExpressionData::Integer(value.clone()))
419    }
420
421    fn visit_typed_integer(
422        &mut self,
423        value: &mut TypedInteger,
424        span: &core::ops::Range<usize>,
425    ) -> Result<VisitAction<DatexExpression>, ()> {
426        self.match_span(span, DatexExpressionData::TypedInteger(value.clone()))
427    }
428
429    fn visit_decimal(
430        &mut self,
431        value: &mut Decimal,
432        span: &core::ops::Range<usize>,
433    ) -> Result<VisitAction<DatexExpression>, ()> {
434        self.match_span(span, DatexExpressionData::Decimal(value.clone()))
435    }
436
437    fn visit_typed_decimal(
438        &mut self,
439        value: &mut TypedDecimal,
440        span: &core::ops::Range<usize>,
441    ) -> Result<VisitAction<DatexExpression>, ()> {
442        self.match_span(span, DatexExpressionData::TypedDecimal(value.clone()))
443    }
444
445    fn visit_text(
446        &mut self,
447        value: &mut String,
448        span: &core::ops::Range<usize>,
449    ) -> Result<VisitAction<DatexExpression>, ()> {
450        self.match_span(span, DatexExpressionData::Text(value.clone()))
451    }
452
453    fn visit_boolean(
454        &mut self,
455        value: &mut bool,
456        span: &core::ops::Range<usize>,
457    ) -> Result<VisitAction<DatexExpression>, ()> {
458        self.match_span(span, DatexExpressionData::Boolean(*value))
459    }
460
461    fn visit_endpoint(
462        &mut self,
463        value: &mut Endpoint,
464        span: &core::ops::Range<usize>,
465    ) -> Result<VisitAction<DatexExpression>, ()> {
466        self.match_span(span, DatexExpressionData::Endpoint(value.clone()))
467    }
468
469    fn visit_null(
470        &mut self,
471        span: &core::ops::Range<usize>,
472    ) -> Result<VisitAction<DatexExpression>, ()> {
473        self.match_span(span, DatexExpressionData::Null)
474    }
475}