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, ¤t_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}