Skip to main content

nifty_lsp/features/
outline.rs

1//! The document outline: top-level declarations, struct fields, and top-level elements
2//! such as windows.
3
4use 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    /// A top-level element such as a window, shown by its `name` if it has one, or else by
53    /// its element, with its title as the detail.
54    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    // `DocumentSymbol::deprecated` is deprecated in favour of `tags` but has no default.
77    #[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
92/// The first argument when it is a plain string, such as a window's title.
93fn 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}