1use std::fmt::Write;
2
3use cairo_lang_defs::db::DefsGroup;
4use cairo_lang_defs::ids::{ImplItemId, LookupItemId, ModuleId, ModuleItemId, TraitItemId};
5use cairo_lang_filesystem::db::FilesGroup;
6use cairo_lang_filesystem::ids::{CrateId, FileId, Tracked};
7use cairo_lang_filesystem::span::{TextOffset, TextSpan, TextWidth};
8use cairo_lang_syntax::node::SyntaxNode;
9use cairo_lang_syntax::node::kind::SyntaxKind;
10use itertools::{Itertools, intersperse};
11use salsa::Database;
12
13use crate::documentable_item::DocumentableItemId;
14use crate::location_links::LocationLink;
15use crate::parser::{DocumentationCommentToken, MarkdownLink, parse_documentation_comment};
16
17pub trait DocGroup: Database {
18 fn get_item_documentation<'db>(&'db self, item_id: DocumentableItemId<'db>) -> Option<String> {
23 get_item_documentation(self.as_dyn_database(), (), item_id)
24 }
25
26 fn get_item_documentation_as_tokens<'db>(
28 &'db self,
29 item_id: DocumentableItemId<'db>,
30 ) -> Option<Vec<DocumentationCommentToken>> {
31 get_item_documentation_as_tokens(self.as_dyn_database(), (), item_id)
32 }
33
34 fn get_item_signature<'db>(&'db self, item_id: DocumentableItemId<'db>) -> Option<String> {
36 self.get_item_signature_with_links(item_id).0
37 }
38
39 fn get_item_signature_with_links<'db>(
42 &'db self,
43 item_id: DocumentableItemId<'db>,
44 ) -> (Option<String>, Vec<LocationLink<'db>>) {
45 crate::documentable_formatter::get_item_signature_with_links(
46 self.as_dyn_database(),
47 (),
48 item_id,
49 )
50 }
51
52 fn get_embedded_markdown_links<'db>(&'db self, node: SyntaxNode<'db>) -> Vec<MarkdownLink> {
54 get_embedded_markdown_links(self.as_dyn_database(), (), node)
55 }
56}
57
58impl<T: Database + ?Sized> DocGroup for T {}
59
60#[salsa::tracked]
61fn get_item_documentation<'db>(
62 db: &'db dyn Database,
63 _tracked: Tracked,
64 item_id: DocumentableItemId<'db>,
65) -> Option<String> {
66 let tokens = db.get_item_documentation_as_tokens(item_id)?;
67 let mut buff = String::new();
68 for doc_token in &tokens {
69 match doc_token {
70 DocumentationCommentToken::Content(content) => buff.push_str(content.as_str()),
71 DocumentationCommentToken::Link(link) => {
72 write!(&mut buff, "[{}]", link.label).ok()?;
73 if let Some(path) = &link.path {
74 write!(&mut buff, "({path})").ok()?;
75 }
76 }
77 }
78 }
79 Some(buff)
80}
81
82#[salsa::tracked]
83fn get_item_documentation_as_tokens<'db>(
84 db: &'db dyn Database,
85 _tracked: Tracked,
86 item_id: DocumentableItemId<'db>,
87) -> Option<Vec<DocumentationCommentToken>> {
88 let (outer_comment, inner_comment, module_level_comment) = match item_id {
89 DocumentableItemId::Crate(crate_id) => {
90 (None, None, get_crate_root_module_documentation(db, crate_id))
91 }
92 item_id => (
93 extract_item_outer_documentation(db, item_id),
96 extract_item_inner_documentation(db, item_id),
101 extract_item_module_level_documentation(db, item_id),
102 ),
103 };
104
105 let outer_comment_tokens =
106 outer_comment.map(|comment| parse_documentation_comment(comment.as_str()));
107 let inner_comment_tokens =
108 inner_comment.map(|comment| parse_documentation_comment(comment.as_str()));
109 let module_level_comment_tokens =
110 module_level_comment.map(|comment| parse_documentation_comment(comment.as_str()));
111
112 let mut result: Vec<Vec<DocumentationCommentToken>> =
113 [module_level_comment_tokens, outer_comment_tokens, inner_comment_tokens]
114 .into_iter()
115 .flatten()
116 .collect();
117 result.retain(|v| !v.is_empty());
118 if result.is_empty() {
119 return None;
120 }
121 let separator_token = vec![DocumentationCommentToken::Content(" ".to_string())];
122 Some(intersperse(result, separator_token).flatten().collect())
123}
124
125fn get_crate_root_module_documentation<'db>(
127 db: &'db dyn Database,
128 crate_id: CrateId<'db>,
129) -> Option<String> {
130 let module_id = db.module_main_file(ModuleId::CrateRoot(crate_id)).ok()?;
131 extract_item_module_level_documentation_from_file(db, module_id)
132}
133
134fn extract_item_inner_documentation<'db>(
136 db: &'db dyn Database,
137 item_id: DocumentableItemId<'db>,
138) -> Option<String> {
139 if matches!(
140 item_id,
141 DocumentableItemId::LookupItem(
142 LookupItemId::ModuleItem(ModuleItemId::FreeFunction(_) | ModuleItemId::Submodule(_))
143 | LookupItemId::ImplItem(ImplItemId::Function(_))
144 | LookupItemId::TraitItem(TraitItemId::Function(_))
145 )
146 ) {
147 let raw_text = item_id
148 .stable_location(db)?
149 .syntax_node(db)
150 .get_text_without_inner_commentable_children(db);
151 Some(extract_item_inner_documentation_from_raw_text(raw_text))
152 } else {
153 None
154 }
155}
156
157fn extract_item_outer_documentation<'db>(
159 db: &'db dyn Database,
160 item_id: DocumentableItemId<'db>,
161) -> Option<String> {
162 let raw_text = item_id.stable_location(db)?.syntax_node(db).get_text(db);
164 Some(
165 raw_text
166 .lines()
167 .filter(|line| !line.trim().is_empty())
168 .take_while_ref(|line| is_comment_line(line) || line.trim_start().starts_with("#"))
171 .filter_map(|line| extract_comment_from_code_line(line, &["///"]))
172 .join("\n"),
173 )
174}
175
176fn extract_item_module_level_documentation<'db>(
178 db: &'db dyn Database,
179 item_id: DocumentableItemId<'db>,
180) -> Option<String> {
181 match item_id {
182 DocumentableItemId::LookupItem(LookupItemId::ModuleItem(ModuleItemId::Submodule(
183 submodule_id,
184 ))) => {
185 if db.is_submodule_inline(submodule_id) {
186 return None;
187 }
188 let module_id = db.module_main_file(ModuleId::Submodule(submodule_id)).ok()?;
189 extract_item_module_level_documentation_from_file(db, module_id)
190 }
191 _ => None,
192 }
193}
194
195fn extract_item_inner_documentation_from_raw_text(raw_text: String) -> String {
197 raw_text
198 .lines()
199 .filter(|line| !line.trim().is_empty())
200 .skip_while(|line| is_comment_line(line))
201 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
202 .join("\n")
203}
204
205fn extract_item_module_level_documentation_from_file<'db>(
207 db: &'db dyn Database,
208 file_id: FileId<'db>,
209) -> Option<String> {
210 let file_content = db.file_content(file_id)?.to_string();
211 Some(
212 file_content
213 .lines()
214 .filter(|line| !line.trim().is_empty())
215 .take_while_ref(|line| is_comment_line(line))
216 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
217 .join("\n"),
218 )
219}
220
221fn extract_comment_from_code_line(line: &str, comment_markers: &[&'static str]) -> Option<String> {
227 let dedent = line.trim_start();
229 for comment_marker in comment_markers {
231 if let Some(content) = dedent.strip_prefix(*comment_marker) {
232 if content.starts_with('/') {
238 return None;
239 }
240 return Some(content.strip_prefix(' ').unwrap_or(content).to_string());
241 }
242 }
243 None
244}
245
246fn is_comment_line(line: &str) -> bool {
248 line.trim_start().starts_with("//")
249}
250
251#[salsa::tracked]
252fn get_embedded_markdown_links<'db>(
254 db: &'db dyn Database,
255 _tracked: Tracked,
256 node: SyntaxNode<'db>,
257) -> Vec<MarkdownLink> {
258 match node.kind(db) {
259 SyntaxKind::TokenSingleLineDocComment | SyntaxKind::TokenSingleLineInnerComment => {
260 let content = node.get_text(db);
261 let base_span = node.span(db);
262 parse_embedded_markdown_links(content, base_span.start)
263 }
264 _ => vec![],
265 }
266}
267
268fn parse_embedded_markdown_links(
270 content: &str,
271 comment_node_offset: TextOffset,
272) -> Vec<MarkdownLink> {
273 let tokens = parse_documentation_comment(content);
274 let mut results = Vec::new();
275
276 for token in tokens {
277 let DocumentationCommentToken::Link(link) = token else { continue };
278 results.push(MarkdownLink {
279 link_span: shift_right_span(comment_node_offset, link.md_link.link_span),
280 dest_span: link
281 .md_link
282 .dest_span
283 .map(|span| shift_right_span(comment_node_offset, span)),
284 dest_text: link.md_link.dest_text,
285 });
286 }
287
288 results
289}
290
291fn shift_right_span(shift_offset: TextOffset, relative_span: TextSpan) -> TextSpan {
295 TextSpan::new(
296 shift_offset.add_width(TextWidth::new_for_testing(relative_span.start.as_u32())),
297 shift_offset.add_width(TextWidth::new_for_testing(relative_span.end.as_u32())),
298 )
299}