nifty_lsp/features/
outline.rs1use lsp_types::{DocumentSymbol, SymbolKind};
5use nifty_syntax::Span;
6use nifty_syntax::ast::{Arg, ExprKind, Ident, Item, Stmt, StmtKind, StringPart};
7
8use crate::document::{Document, Encoding};
9
10pub(crate) fn outline(document: &Document, encoding: Encoding) -> Vec<DocumentSymbol> {
11 let outline = Outline { document, encoding };
12 document
13 .parsed
14 .file
15 .items
16 .iter()
17 .filter_map(|item| outline.item(item))
18 .collect()
19}
20
21struct Outline<'doc> {
22 document: &'doc Document,
23 encoding: Encoding,
24}
25
26impl Outline<'_> {
27 fn item(&self, item: &Item) -> Option<DocumentSymbol> {
28 let symbol = match item {
29 Item::State(decl) => self.declaration(&decl.name, SymbolKind::VARIABLE, decl.span),
30 Item::Fn(decl) => self.declaration(&decl.name, SymbolKind::FUNCTION, decl.span),
31 Item::View(decl) => self.declaration(&decl.name, SymbolKind::CLASS, decl.span),
32 Item::Struct(decl) => {
33 let fields = decl
34 .fields
35 .iter()
36 .map(|field| {
37 let span = field.name.span.to(field.ty.span);
38 self.declaration(&field.name, SymbolKind::FIELD, span)
39 })
40 .collect();
41 DocumentSymbol {
42 children: Some(fields),
43 ..self.declaration(&decl.name, SymbolKind::STRUCT, decl.span)
44 }
45 }
46 Item::Named(named) => self.element(&named.call, Some(&named.name))?,
47 Item::Stmt(stmt) => self.element(stmt, None)?,
48 };
49 Some(symbol)
50 }
51
52 fn element(&self, stmt: &Stmt, name: Option<&Ident>) -> Option<DocumentSymbol> {
55 let StmtKind::Expr(expr) = &stmt.kind else {
56 return None;
57 };
58 let ExprKind::Call { callee, args, .. } = &expr.kind else {
59 return None;
60 };
61 let ExprKind::Name(element) = &callee.kind else {
62 return None;
63 };
64 let (shown, shown_span) =
65 name.map_or((element, callee.span), |name| (&name.name, name.span));
66 Some(DocumentSymbol {
67 detail: title(args),
68 ..self.symbol(shown, shown_span, SymbolKind::OBJECT, stmt.span)
69 })
70 }
71
72 fn declaration(&self, name: &Ident, kind: SymbolKind, span: Span) -> DocumentSymbol {
73 self.symbol(&name.name, name.span, kind, span)
74 }
75
76 #[allow(deprecated)]
78 fn symbol(&self, name: &str, name_span: Span, kind: SymbolKind, span: Span) -> DocumentSymbol {
79 DocumentSymbol {
80 name: name.to_owned(),
81 detail: None,
82 kind,
83 tags: None,
84 deprecated: None,
85 range: self.document.range(span, self.encoding),
86 selection_range: self.document.range(name_span, self.encoding),
87 children: None,
88 }
89 }
90}
91
92fn title(args: &[Arg]) -> Option<String> {
94 let first = args.first().filter(|arg| arg.label.is_none())?;
95 let ExprKind::String(parts) = &first.value.kind else {
96 return None;
97 };
98 parts
99 .iter()
100 .map(|part| match part {
101 StringPart::Text(text) => Some(text.as_str()),
102 StringPart::Expr(_) => None,
103 })
104 .collect()
105}
106
107#[cfg(test)]
108mod tests {
109 use nifty_syntax::FileId;
110
111 use super::*;
112
113 #[test]
114 fn lists_declarations_fields_and_windows() {
115 let source = "struct P { x: int }\nstate n = 0\nwindow(\"App\") {}";
116 let document = Document::new(source.to_owned(), FileId::default());
117 let symbols = outline(&document, Encoding::Utf16);
118 let summary: Vec<_> = symbols
119 .iter()
120 .map(|symbol| {
121 let children: Vec<_> = symbol
122 .children
123 .iter()
124 .flatten()
125 .map(|child| child.name.as_str())
126 .collect();
127 (symbol.name.as_str(), symbol.detail.as_deref(), children)
128 })
129 .collect();
130 assert_eq!(
131 summary,
132 [
133 ("P", None, vec!["x"]),
134 ("n", None, vec![]),
135 ("window", Some("App"), vec![])
136 ]
137 );
138 }
139}