shape_lsp/completion/
annotations.rs1use 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
12pub 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
57pub 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
71pub 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#[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}