Skip to main content

shape_lsp/completion/
docs.rs

1use crate::doc_symbols::{
2    collect_import_paths, collect_program_doc_symbols, current_module_import_path, find_doc_owner,
3    span_contains,
4};
5use crate::module_cache::ModuleCache;
6use shape_ast::ast::{DocTagKind, DocTargetKind, Program};
7use std::collections::{BTreeSet, HashSet};
8use std::path::Path;
9use tower_lsp_server::ls_types::{CompletionItem, CompletionItemKind};
10
11const DOC_TAGS: [(&str, &str); 11] = [
12    ("module", "Canonical module name for this declaration"),
13    ("typeparam", "Describe a generic type parameter"),
14    ("param", "Describe a callable parameter"),
15    ("returns", "Describe the return value"),
16    ("throws", "Describe an error case"),
17    ("deprecated", "Mark the symbol as deprecated"),
18    ("requires", "Describe availability requirements"),
19    ("since", "Record the introduction version or milestone"),
20    ("see", "Reference another fully qualified symbol"),
21    (
22        "link",
23        "Reference another fully qualified symbol with an optional label",
24    ),
25    ("note", "Record an additional implementation or usage note"),
26];
27
28pub fn doc_tag_completions(prefix: &str) -> Vec<CompletionItem> {
29    DOC_TAGS
30        .into_iter()
31        .filter(|(tag, _)| prefix.is_empty() || tag.starts_with(prefix))
32        .map(|(tag, detail)| CompletionItem {
33            label: tag.to_string(),
34            kind: Some(CompletionItemKind::KEYWORD),
35            detail: Some(detail.to_string()),
36            insert_text: Some(tag.to_string()),
37            sort_text: Some(format!("0_{}", tag)),
38            ..Default::default()
39        })
40        .collect()
41}
42
43pub fn doc_param_completions(
44    program: &Program,
45    cursor_offset: usize,
46    prefix: &str,
47) -> Vec<CompletionItem> {
48    let Some(context) = doc_owner_context(program, cursor_offset) else {
49        return Vec::new();
50    };
51
52    completion_items_for_names(
53        context.params,
54        &context.documented_params,
55        prefix,
56        "function parameter",
57        CompletionItemKind::VARIABLE,
58    )
59}
60
61pub fn doc_type_param_completions(
62    program: &Program,
63    cursor_offset: usize,
64    prefix: &str,
65) -> Vec<CompletionItem> {
66    let Some(context) = doc_owner_context(program, cursor_offset) else {
67        return Vec::new();
68    };
69
70    completion_items_for_names(
71        context.type_params,
72        &context.documented_type_params,
73        prefix,
74        "type parameter",
75        CompletionItemKind::TYPE_PARAMETER,
76    )
77}
78
79pub fn doc_link_completions(
80    program: &Program,
81    prefix: &str,
82    module_cache: Option<&ModuleCache>,
83    current_file: Option<&Path>,
84    workspace_root: Option<&Path>,
85) -> Vec<CompletionItem> {
86    let mut candidates = Vec::new();
87    let mut seen = BTreeSet::new();
88
89    if let Some(current_module_path) =
90        current_module_import_path(module_cache, current_file, workspace_root)
91    {
92        for symbol in collect_program_doc_symbols(program, &current_module_path) {
93            if seen.insert(symbol.qualified_path.clone()) {
94                candidates.push((
95                    symbol.qualified_path,
96                    completion_kind_for_doc_symbol(symbol.kind),
97                    "workspace symbol",
98                    0,
99                ));
100            }
101        }
102        if seen.insert(current_module_path.clone()) {
103            candidates.push((
104                current_module_path,
105                CompletionItemKind::MODULE,
106                "current module",
107                0,
108            ));
109        }
110    }
111
112    let imported_modules = collect_import_paths(program);
113    if let (Some(cache), Some(file_path)) = (module_cache, current_file) {
114        for import_path in &imported_modules {
115            push_module_link_candidates(
116                &mut candidates,
117                &mut seen,
118                cache,
119                import_path,
120                file_path,
121                workspace_root,
122                1,
123            );
124        }
125
126        for module_path in cache.list_importable_modules_with_context(file_path, workspace_root) {
127            if imported_modules.contains(&module_path) {
128                continue;
129            }
130            let priority = if module_path.starts_with("std::") {
131                3
132            } else {
133                2
134            };
135            push_module_link_candidates(
136                &mut candidates,
137                &mut seen,
138                cache,
139                &module_path,
140                file_path,
141                workspace_root,
142                priority,
143            );
144        }
145    }
146
147    candidates
148        .into_iter()
149        .filter(|(target, _, _, _)| prefix.is_empty() || target.starts_with(prefix))
150        .map(|(target, kind, detail, priority)| CompletionItem {
151            label: target.clone(),
152            insert_text: Some(target.clone()),
153            kind: Some(kind),
154            detail: Some(detail.to_string()),
155            sort_text: Some(format!("{priority}_{target}")),
156            ..Default::default()
157        })
158        .collect()
159}
160
161struct DocOwnerContext {
162    params: Vec<String>,
163    type_params: Vec<String>,
164    documented_params: HashSet<String>,
165    documented_type_params: HashSet<String>,
166}
167
168fn doc_owner_context(program: &Program, cursor_offset: usize) -> Option<DocOwnerContext> {
169    let entry = program
170        .docs
171        .entries
172        .iter()
173        .find(|entry| span_contains(entry.comment.span, cursor_offset))?;
174    let owner = find_doc_owner(program, entry.target.span)?;
175
176    let documented_params = entry
177        .comment
178        .tags
179        .iter()
180        .filter_map(|tag| match (&tag.kind, &tag.name) {
181            (DocTagKind::Param, Some(name)) => Some(name.clone()),
182            _ => None,
183        })
184        .collect();
185    let documented_type_params = entry
186        .comment
187        .tags
188        .iter()
189        .filter_map(|tag| match (&tag.kind, &tag.name) {
190            (DocTagKind::TypeParam, Some(name)) => Some(name.clone()),
191            _ => None,
192        })
193        .collect();
194
195    Some(DocOwnerContext {
196        params: owner.params,
197        type_params: owner.type_params,
198        documented_params,
199        documented_type_params,
200    })
201}
202
203fn completion_items_for_names(
204    names: Vec<String>,
205    already_documented: &HashSet<String>,
206    prefix: &str,
207    detail: &str,
208    kind: CompletionItemKind,
209) -> Vec<CompletionItem> {
210    names
211        .into_iter()
212        .filter(|name| prefix.is_empty() || name.starts_with(prefix))
213        .map(|name| {
214            let priority = if already_documented.contains(&name) {
215                1
216            } else {
217                0
218            };
219            CompletionItem {
220                label: name.clone(),
221                insert_text: Some(name.clone()),
222                kind: Some(kind),
223                detail: Some(detail.to_string()),
224                sort_text: Some(format!("{priority}_{name}")),
225                ..Default::default()
226            }
227        })
228        .collect()
229}
230
231fn push_module_link_candidates(
232    candidates: &mut Vec<(String, CompletionItemKind, &'static str, usize)>,
233    seen: &mut BTreeSet<String>,
234    module_cache: &ModuleCache,
235    import_path: &str,
236    current_file: &Path,
237    workspace_root: Option<&Path>,
238    priority: usize,
239) {
240    if seen.insert(import_path.to_string()) {
241        candidates.push((
242            import_path.to_string(),
243            CompletionItemKind::MODULE,
244            "module",
245            priority,
246        ));
247    }
248
249    let Some(resolved) = module_cache.resolve_import(import_path, current_file, workspace_root)
250    else {
251        return;
252    };
253    let Some(module_info) =
254        module_cache.load_module_with_context(&resolved, current_file, workspace_root)
255    else {
256        return;
257    };
258
259    for symbol in collect_program_doc_symbols(&module_info.program, import_path) {
260        if seen.insert(symbol.qualified_path.clone()) {
261            candidates.push((
262                symbol.qualified_path,
263                completion_kind_for_doc_symbol(symbol.kind),
264                "module symbol",
265                priority,
266            ));
267        }
268    }
269}
270
271fn completion_kind_for_doc_symbol(kind: DocTargetKind) -> CompletionItemKind {
272    match kind {
273        DocTargetKind::Function
274        | DocTargetKind::Annotation
275        | DocTargetKind::ForeignFunction
276        | DocTargetKind::BuiltinFunction
277        | DocTargetKind::ExtensionMethod
278        | DocTargetKind::ImplMethod
279        | DocTargetKind::TraitMethod => CompletionItemKind::FUNCTION,
280        DocTargetKind::Struct
281        | DocTargetKind::Enum
282        | DocTargetKind::Trait
283        | DocTargetKind::TypeAlias
284        | DocTargetKind::BuiltinType => CompletionItemKind::CLASS,
285        DocTargetKind::Module => CompletionItemKind::MODULE,
286        DocTargetKind::TypeParam => CompletionItemKind::TYPE_PARAMETER,
287        DocTargetKind::StructField | DocTargetKind::EnumVariant => CompletionItemKind::FIELD,
288        DocTargetKind::TraitProperty | DocTargetKind::TraitIndexSignature => {
289            CompletionItemKind::PROPERTY
290        }
291        DocTargetKind::TraitAssociatedType => CompletionItemKind::INTERFACE,
292    }
293}