Skip to main content

nifty_lsp/
index.rs

1//! Every name in a project's files, each tied to the declaration it refers to when one
2//! exists in the project. Built-in elements, built-in types, fields reached through a value and implicit
3//! members have no declaration until the type checker exists, so they stay unresolved.
4
5mod build;
6
7use nifty_syntax::ast::File;
8use nifty_syntax::{FileId, Span};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub(crate) struct SymbolId(usize);
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub(crate) enum SymbolKind {
15    State,
16    Struct,
17    Field,
18    Function,
19    View,
20    Window,
21    Parameter,
22    Local,
23}
24
25/// Where in the project a symbol's name can be used.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub(crate) enum Visibility {
28    /// Top-level declarations, which may be used before they are declared and in every file.
29    Everywhere,
30    /// Parameters and `for` items, inside their body.
31    Within(Span),
32    /// Locals, from the end of their `let` to the end of the enclosing block or file.
33    After {
34        file: FileId,
35        start: usize,
36        end: Option<usize>,
37    },
38}
39
40impl Visibility {
41    fn contains(self, file: FileId, offset: usize) -> bool {
42        match self {
43            Self::Everywhere => true,
44            Self::Within(span) => span.file == file && span.start < offset && offset < span.end,
45            Self::After {
46                file: declared_in,
47                start,
48                end,
49            } => declared_in == file && start <= offset && end.is_none_or(|end| offset < end),
50        }
51    }
52}
53
54#[derive(Debug)]
55pub(crate) struct Symbol {
56    pub name: String,
57    pub kind: SymbolKind,
58    /// The struct a field belongs to, or the function or view a parameter belongs to.
59    pub parent: Option<SymbolId>,
60    pub visibility: Visibility,
61}
62
63/// What a name without a declaration in the project is used as.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub(crate) enum Unresolved {
66    Function,
67    Type,
68    Variable,
69    Field,
70    Method,
71    ImplicitMember,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub(crate) enum Role {
76    Declaration(SymbolId),
77    Reference(SymbolId),
78    Unresolved(Unresolved),
79}
80
81impl Role {
82    pub(crate) fn symbol(self) -> Option<SymbolId> {
83        match self {
84            Self::Declaration(id) | Self::Reference(id) => Some(id),
85            Self::Unresolved(_) => None,
86        }
87    }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub(crate) struct Occurrence {
92    pub span: Span,
93    pub role: Role,
94}
95
96#[derive(Debug)]
97pub(crate) struct Index {
98    symbols: Vec<Symbol>,
99    /// Sorted by file, then position. Names never overlap, so within a file this is also
100    /// sorted by end.
101    occurrences: Vec<Occurrence>,
102}
103
104impl Index {
105    /// Indexes `files` as one program, which share their top-level names.
106    pub(crate) fn new(files: &[&File]) -> Self {
107        build::build(files)
108    }
109
110    pub(crate) fn symbol(&self, id: SymbolId) -> &Symbol {
111        &self.symbols[id.0]
112    }
113
114    /// The names in `file`, in source order.
115    pub(crate) fn occurrences_in(&self, file: FileId) -> &[Occurrence] {
116        let start = self
117            .occurrences
118            .partition_point(|occurrence| occurrence.span.file < file);
119        let end = self
120            .occurrences
121            .partition_point(|occurrence| occurrence.span.file <= file);
122        &self.occurrences[start..end]
123    }
124
125    /// The name covering `offset` in `file`, including a cursor sitting just past its last
126    /// character.
127    pub(crate) fn occurrence_at(&self, file: FileId, offset: usize) -> Option<Occurrence> {
128        let in_file = self.occurrences_in(file);
129        let first_ending_at_or_after =
130            in_file.partition_point(|occurrence| occurrence.span.end < offset);
131        in_file
132            .get(first_ending_at_or_after)
133            .filter(|occurrence| occurrence.span.start <= offset)
134            .copied()
135    }
136
137    /// The declaration and every reference of `id`, file by file in source order.
138    pub(crate) fn occurrences_of(&self, id: SymbolId) -> impl Iterator<Item = Occurrence> {
139        self.occurrences
140            .iter()
141            .filter(move |occurrence| occurrence.role.symbol() == Some(id))
142            .copied()
143    }
144
145    /// The symbols whose names can be used at `offset` in `file`. Fields are only reached
146    /// through a value or an argument label, so they are never in scope by themselves.
147    pub(crate) fn in_scope(&self, file: FileId, offset: usize) -> impl Iterator<Item = &Symbol> {
148        self.symbols.iter().filter(move |symbol| {
149            symbol.kind != SymbolKind::Field && symbol.visibility.contains(file, offset)
150        })
151    }
152
153    pub(crate) fn declaration_of(&self, id: SymbolId) -> Option<Span> {
154        self.occurrences
155            .iter()
156            .find(|occurrence| occurrence.role == Role::Declaration(id))
157            .map(|occurrence| occurrence.span)
158    }
159}
160
161#[cfg(test)]
162mod tests;