Skip to main content

shape_lsp/completion/
annotations.rs

1//! Annotation completions for @decorator syntax
2
3use crate::annotation_discovery::{AnnotationDiscovery, render_annotation_documentation};
4use crate::module_cache::ModuleCache;
5use crate::symbols::{SymbolInfo, symbols_to_completions};
6use shape_ast::ast::Program;
7use std::path::Path;
8use tower_lsp_server::ls_types::{
9    CompletionItem, CompletionItemKind, Documentation, InsertTextFormat,
10};
11
12/// Annotation completions after typing "@"
13pub fn annotation_completions(
14    annotation_discovery: &AnnotationDiscovery,
15    program: Option<&Program>,
16    module_cache: Option<&ModuleCache>,
17    current_file: Option<&Path>,
18    workspace_root: Option<&Path>,
19) -> Vec<CompletionItem> {
20    annotation_discovery
21        .all_annotations()
22        .into_iter()
23        .map(|ann| {
24            let params_str = ann.params.join(", ");
25            let insert_text = if ann.params.is_empty() {
26                ann.name.clone()
27            } else {
28                format!("{}(${{1}})", ann.name)
29            };
30
31            let detail = if ann.params.is_empty() {
32                format!("@{}", ann.name)
33            } else {
34                format!("@{}({})", ann.name, params_str)
35            };
36
37            CompletionItem {
38                label: format!("@{}", ann.name),
39                kind: Some(CompletionItemKind::KEYWORD),
40                detail: Some(detail),
41                documentation: render_annotation_documentation(
42                    ann,
43                    program,
44                    module_cache,
45                    current_file,
46                    workspace_root,
47                )
48                .map(Documentation::String),
49                insert_text: Some(insert_text),
50                insert_text_format: Some(InsertTextFormat::SNIPPET),
51                ..Default::default()
52            }
53        })
54        .collect()
55}
56
57/// Filter symbols by annotation
58pub fn symbols_with_annotation(
59    annotation_name: &str,
60    user_symbols: &[SymbolInfo],
61) -> Vec<CompletionItem> {
62    let filtered: Vec<_> = user_symbols
63        .iter()
64        .filter(|s| s.annotations.iter().any(|a| a == annotation_name))
65        .cloned()
66        .collect();
67
68    symbols_to_completions(&filtered)
69}
70
71/// Enum value completions for allowed_values constraints
72pub fn enum_value_completions(values: &[String]) -> Vec<CompletionItem> {
73    values
74        .iter()
75        .map(|value| CompletionItem {
76            label: value.clone(),
77            kind: Some(CompletionItemKind::ENUM_MEMBER),
78            detail: Some("Allowed value".to_string()),
79            insert_text: Some(format!("\"{}\"", value)),
80            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
81            ..Default::default()
82        })
83        .collect()
84}
85
86/// Check if cursor is at annotation position
87#[allow(dead_code)]
88pub fn is_at_annotation_position(text: &str) -> bool {
89    let trimmed = text.trim_end();
90    if trimmed.ends_with('@') {
91        let before_at = trimmed.trim_end_matches('@').trim_end();
92        return before_at.is_empty() || before_at.ends_with('\n');
93    }
94    false
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use shape_ast::parser::parse_program;
101
102    #[test]
103    fn annotation_completion_uses_doc_comments() {
104        let program = parse_program(
105            "/// Trace function execution.\nannotation trace() {\n    metadata() { return { traced: true } }\n}\n",
106        )
107        .expect("program");
108        let mut discovery = AnnotationDiscovery::new();
109        discovery.discover_from_program(&program);
110
111        let completions = annotation_completions(&discovery, Some(&program), None, None, None);
112        let trace = completions
113            .iter()
114            .find(|item| item.label == "@trace")
115            .expect("trace completion");
116        let Some(Documentation::String(doc)) = trace.documentation.as_ref() else {
117            panic!("expected annotation documentation");
118        };
119        assert!(doc.contains("Trace function execution."));
120        assert!(!doc.contains("Handlers:"));
121    }
122}