mortar_lsp 0.4.3

Mortar language LSP server
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! # completion.rs
//!
//! # completion.rs 文件
//!
//! ## Module Overview
//!
//! ## 模块概述
//!
//! Implements completion-context analysis and completion-item generation for the Mortar
//! LSP backend. It inspects document structure, determines whether the cursor is in top-level,
//! node, choice, or expression context, and then emits the matching keyword and symbol suggestions.
//!
//! 负责 Mortar LSP backend 的补全上下文分析和补全项生成。它会检查文档结构,判断光标
//! 当前位于顶层、节点体、选项区还是表达式区,然后生成对应的关键字和符号建议。

use tower_lsp_server::lsp_types::*;

use crate::analysis::SymbolTable;
use crate::backend::Backend;

/// Autocomplete context type
#[derive(Debug, Clone, PartialEq)]
pub enum CompletionContext {
    TopLevel,
    InNode,
    InChoice,
    InExpression,
}

impl Backend {
    /// Context analysis based on entire document
    pub fn analyze_document_context(&self, uri: &Uri, position: Position) -> CompletionContext {
        let Some(document_entry) = self.documents.get(uri) else {
            return CompletionContext::TopLevel;
        };

        let rope = &document_entry.0;
        let line_idx = position.line as usize;

        let mut brace_depth = 0;
        let mut in_node = false;
        let mut in_function = false;

        for i in 0..=line_idx.min(rope.len_lines() - 1) {
            let line_content = rope.line(i).to_string();
            let trimmed = line_content.trim();

            let is_node_decl = trimmed.starts_with("node ") || trimmed.starts_with("nd ");
            let is_fn_decl = trimmed.starts_with("fn ") || trimmed.starts_with("function ");

            if is_node_decl && brace_depth == 0 {
                in_node = false;
            }
            if is_fn_decl && brace_depth == 0 {
                in_function = false;
            }

            Self::update_brace_state(
                &line_content,
                is_node_decl,
                is_fn_decl,
                &mut brace_depth,
                &mut in_node,
                &mut in_function,
            );

            if i == line_idx {
                break;
            }
        }

        if line_idx < rope.len_lines() {
            let current_line = rope.line(line_idx).to_string();
            let current_trimmed = current_line.trim();
            let is_choice = current_trimmed.contains("choice")
                || current_trimmed.contains("->")
                || (in_node && current_trimmed.contains("\""));
            if is_choice {
                return CompletionContext::InChoice;
            }
        }

        if in_function && brace_depth > 0 {
            CompletionContext::InExpression
        } else if in_node && brace_depth > 0 {
            CompletionContext::InNode
        } else {
            CompletionContext::TopLevel
        }
    }

    fn update_brace_state(
        line_content: &str,
        is_node_decl: bool,
        is_fn_decl: bool,
        brace_depth: &mut i32,
        in_node: &mut bool,
        in_function: &mut bool,
    ) {
        let open_count = line_content.chars().filter(|&c| c == '{').count() as i32;
        let close_count = line_content.chars().filter(|&c| c == '}').count() as i32;

        if open_count > 0 {
            *brace_depth += open_count;
            if is_node_decl {
                *in_node = true;
                *in_function = false;
            } else if is_fn_decl {
                *in_function = true;
                *in_node = false;
            }
        }

        if close_count > 0 {
            *brace_depth -= close_count;
            if *brace_depth == 0 {
                *in_node = false;
                *in_function = false;
            }
        }
    }

    /// Analyze the context of auto-completion
    pub fn analyze_completion_context(&self, line: &str, _char_idx: usize) -> CompletionContext {
        let trimmed = line.trim();

        if trimmed.is_empty() || (!trimmed.contains('{') && !trimmed.contains('}')) {
            return CompletionContext::TopLevel;
        }

        if trimmed.starts_with("node ")
            || trimmed.starts_with("nd ")
            || trimmed.starts_with("fn ")
            || trimmed.starts_with("function ")
        {
            return if trimmed.ends_with('{') {
                CompletionContext::InNode
            } else {
                CompletionContext::TopLevel
            };
        }

        if trimmed.contains("choice:") || trimmed.contains("->") {
            return CompletionContext::InChoice;
        }

        if trimmed.starts_with("text:") || trimmed.starts_with("events:") {
            return CompletionContext::InNode;
        }

        CompletionContext::TopLevel
    }

    pub fn generate_completion_items(&self, context: CompletionContext) -> Vec<CompletionItem> {
        match context {
            CompletionContext::TopLevel => vec![
                CompletionItem {
                    label: "node".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Define a new story node".to_string()),
                    insert_text: Some("node ${1:node_name} {\n    text: \"${2:text_content}\"\n}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "nd".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Define a new story node (short form)".to_string()),
                    insert_text: Some("nd ${1:node_name} {\n    text: \"${2:text_content}\"\n}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "fn".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Define a function".to_string()),
                    insert_text: Some("fn ${1:function_name}(${2:params}) -> ${3:ReturnType} {\n    ${4:// function body}\n}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
            ],
            CompletionContext::InNode => vec![
                CompletionItem {
                    label: "text".to_string(),
                    kind: Some(CompletionItemKind::PROPERTY),
                    detail: Some("Story text content".to_string()),
                    insert_text: Some("text: \"${1:text_content}\"".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "choice".to_string(),
                    kind: Some(CompletionItemKind::PROPERTY),
                    detail: Some("Player choices".to_string()),
                    insert_text: Some("choice: [\n    \"${1:choice_text}\" -> ${2:target_node}\n]".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "events".to_string(),
                    kind: Some(CompletionItemKind::PROPERTY),
                    detail: Some("Timed events".to_string()),
                    insert_text: Some("events: [\n    ${1:delay}, ${2:action}\n]".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
            ],
            CompletionContext::InChoice => vec![
                CompletionItem {
                    label: "when".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Conditional choice".to_string()),
                    insert_text: Some("when ${1:condition}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "->".to_string(),
                    kind: Some(CompletionItemKind::OPERATOR),
                    detail: Some("Jump to target node".to_string()),
                    insert_text: Some("-> ${1:target_node}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
            ],
            CompletionContext::InExpression => vec![
                CompletionItem {
                    label: "return".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Return statement".to_string()),
                    insert_text: Some("return ${1:value}".to_string()),
                    insert_text_format: Some(InsertTextFormat::SNIPPET),
                    ..Default::default()
                },
                CompletionItem {
                    label: "break".to_string(),
                    kind: Some(CompletionItemKind::KEYWORD),
                    detail: Some("Break statement".to_string()),
                    ..Default::default()
                },
            ],
        }
    }

    /// Generate filtered completion items based on current word and symbol table
    pub fn generate_completion_items_filtered(
        &self,
        context: CompletionContext,
        current_word: &str,
        symbol_table: &SymbolTable,
    ) -> Vec<CompletionItem> {
        match context {
            CompletionContext::TopLevel => Self::completions_top_level(current_word),
            CompletionContext::InNode => Self::completions_in_node(current_word),
            CompletionContext::InChoice => Self::completions_in_choice(current_word, symbol_table),
            CompletionContext::InExpression => {
                Self::completions_in_expression(current_word, symbol_table)
            }
        }
    }

    fn completions_top_level(current_word: &str) -> Vec<CompletionItem> {
        let mut completions = Vec::new();

        if "node".starts_with(current_word) {
            completions.push(CompletionItem {
                label: "node".to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                insert_text: Some("node ${1:node_name} {\n\t$0\n}".to_string()),
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                detail: Some("Create a dialogue node".to_string()),
                documentation: Some(Documentation::String(
                    "Create a new dialog node that defines dialog content and choices".to_string(),
                )),
                ..CompletionItem::default()
            });
        }

        if "nd".starts_with(current_word) {
            completions.push(CompletionItem {
                label: "nd".to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                insert_text: Some("nd ${1:node_name} {\n\t$0\n}".to_string()),
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                detail: Some("Create a dialogue node".to_string()),
                documentation: Some(Documentation::String(
                    "Create a new dialog node that defines dialog content and choices".to_string(),
                )),
                ..CompletionItem::default()
            });
        }

        if "fn".starts_with(current_word) {
            completions.push(CompletionItem {
                label: "fn".to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                insert_text: Some("fn ${1:function_name}()".to_string()),
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                detail: Some("Create a function".to_string()),
                documentation: Some(Documentation::String(
                    "Create a new function definition".to_string(),
                )),
                ..CompletionItem::default()
            });
        }

        if "function".starts_with(current_word) {
            completions.push(CompletionItem {
                label: "function".to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                insert_text: Some("function ${1:function_name}() {\n\t$0\n}".to_string()),
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                detail: Some("Create a function".to_string()),
                documentation: Some(Documentation::String(
                    "Create a new function definition (alternative syntax)".to_string(),
                )),
                ..CompletionItem::default()
            });
        }

        completions
    }

    fn completions_in_node(current_word: &str) -> Vec<CompletionItem> {
        let mut completions = Vec::new();

        for keyword in &["text", "choice", "events"] {
            if !keyword.starts_with(current_word) {
                continue;
            }
            let (insert_text, detail) = match *keyword {
                "text" => (
                    "text: \"${1:text_content}\"".to_string(),
                    "Story text content".to_string(),
                ),
                "choice" => (
                    "choice: [\n\t\"${1:text}\" -> ${2:target_node}\n]".to_string(),
                    "Add selection".to_string(),
                ),
                "events" => (
                    "events: [\n\t${1:delay}, ${2:action}\n]".to_string(),
                    "Timed events".to_string(),
                ),
                _ => (keyword.to_string(), "".to_string()),
            };
            completions.push(CompletionItem {
                label: keyword.to_string(),
                kind: Some(CompletionItemKind::PROPERTY),
                insert_text: Some(insert_text),
                insert_text_format: Some(InsertTextFormat::SNIPPET),
                detail: Some(detail),
                ..CompletionItem::default()
            });
        }

        completions
    }

    fn completions_in_choice(
        current_word: &str,
        symbol_table: &SymbolTable,
    ) -> Vec<CompletionItem> {
        let mut completions = Vec::new();

        for keyword in &["when", "return", "break"] {
            if !keyword.starts_with(current_word) {
                continue;
            }
            let insert_text = match *keyword {
                "when" => Some("when ${1:condition}".to_string()),
                "return" => Some("return ${1:value}".to_string()),
                _ => None,
            };
            let insert_text_format = insert_text.as_ref().map(|_| InsertTextFormat::SNIPPET);
            completions.push(CompletionItem {
                label: keyword.to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                insert_text,
                insert_text_format,
                ..CompletionItem::default()
            });
        }

        // Add node completions
        for node in &symbol_table.nodes {
            if !node.starts_with(current_word) {
                continue;
            }
            completions.push(CompletionItem {
                label: node.clone(),
                kind: Some(CompletionItemKind::CLASS),
                detail: Some("Jump to node".to_string()),
                ..CompletionItem::default()
            });
        }

        completions
    }

    fn completions_in_expression(
        current_word: &str,
        symbol_table: &SymbolTable,
    ) -> Vec<CompletionItem> {
        let mut completions = Vec::new();

        // Add function completions
        for func in &symbol_table.functions {
            if !func.name.starts_with(current_word) {
                continue;
            }
            let return_suffix = match func.return_type.as_ref().filter(|t| !t.is_empty()) {
                Some(rt) => format!(" -> {rt}"),
                None => String::new(),
            };
            let params_str = func
                .params
                .iter()
                .map(|p| format!("{}: {}", p.name, p.type_name))
                .collect::<Vec<_>>()
                .join(", ");
            completions.push(CompletionItem {
                label: func.name.clone(),
                kind: Some(CompletionItemKind::FUNCTION),
                detail: Some(format!("fn {}({}){}", func.name, params_str, return_suffix)),
                ..CompletionItem::default()
            });
        }

        // Add type keywords
        for type_keyword in &["String", "Number", "Boolean", "Bool", "true", "false"] {
            if !type_keyword.starts_with(current_word) {
                continue;
            }
            completions.push(CompletionItem {
                label: type_keyword.to_string(),
                kind: Some(CompletionItemKind::KEYWORD),
                ..CompletionItem::default()
            });
        }

        completions
    }
}