Skip to main content

nifty_lsp/
project.rs

1//! The files of one program, analysed together: a project folder's `.nf` files, or a single
2//! file outside any project. A name declared in one file resolves in all of them.
3
4use lsp_types::{Location, Uri};
5use nifty_syntax::ast::{File, Item};
6use nifty_syntax::{Diagnostic, FileId, Parsed, Span};
7
8use crate::document::{Document, Encoding};
9use crate::index::Index;
10
11/// A source file's text and the address the editor knows it by.
12#[derive(Debug)]
13pub(crate) struct Source {
14    pub uri: Uri,
15    pub text: String,
16}
17
18#[derive(Debug)]
19pub(crate) struct Project {
20    /// File `i` is `uris[i]` and `documents[i]`.
21    uris: Vec<Uri>,
22    documents: Vec<Document>,
23    pub index: Index,
24    /// Syntax errors from every file, or type errors once they all parse.
25    diagnostics: Vec<Diagnostic>,
26}
27
28impl Project {
29    /// Analyses `sources` as one program; the first is the entry file.
30    pub(crate) fn new(sources: Vec<Source>) -> Self {
31        let (uris, documents): (Vec<Uri>, Vec<Document>) = sources
32            .into_iter()
33            .enumerate()
34            .map(|(index, source)| (source.uri, Document::new(source.text, FileId::new(index))))
35            .unzip();
36        let parsed: Vec<&Parsed> = documents.iter().map(|document| &document.parsed).collect();
37        let files: Vec<&File> = parsed.iter().map(|parsed| &parsed.file).collect();
38        let index = Index::new(&files);
39        let diagnostics = nifty_check::compile(&parsed).err().unwrap_or_default();
40        Self {
41            uris,
42            documents,
43            index,
44            diagnostics,
45        }
46    }
47
48    pub(crate) fn file_of(&self, uri: &Uri) -> Option<FileId> {
49        self.uris
50            .iter()
51            .position(|known| known == uri)
52            .map(FileId::new)
53    }
54
55    /// Every file with its address.
56    pub(crate) fn files(&self) -> impl Iterator<Item = (FileId, &Uri)> {
57        self.uris
58            .iter()
59            .enumerate()
60            .map(|(index, uri)| (FileId::new(index), uri))
61    }
62
63    pub(crate) fn document(&self, file: FileId) -> &Document {
64        &self.documents[file.index()]
65    }
66
67    /// The text of `span`, in whichever file it points into.
68    pub(crate) fn text(&self, span: Span) -> &str {
69        self.document(span.file).text(span)
70    }
71
72    pub(crate) fn location(&self, span: Span, encoding: Encoding) -> Location {
73        Location::new(
74            self.uris[span.file.index()].clone(),
75            self.document(span.file).range(span, encoding),
76        )
77    }
78
79    /// The errors whose main place is in `file`.
80    pub(crate) fn diagnostics_in(&self, file: FileId) -> impl Iterator<Item = &Diagnostic> {
81        self.diagnostics
82            .iter()
83            .filter(move |diagnostic| diagnostic.span.file == file)
84    }
85
86    /// The top-level items of every file.
87    pub(crate) fn items(&self) -> impl Iterator<Item = &Item> {
88        self.documents
89            .iter()
90            .flat_map(|document| &document.parsed.file.items)
91    }
92}
93
94/// The place in a project a request points at: an offset in one of its files.
95#[derive(Clone, Copy, Debug)]
96pub(crate) struct Cursor<'p> {
97    pub project: &'p Project,
98    pub file: FileId,
99    pub offset: usize,
100}
101
102impl<'p> Cursor<'p> {
103    pub(crate) fn document(self) -> &'p Document {
104        self.project.document(self.file)
105    }
106}
107
108#[cfg(test)]
109impl Project {
110    /// A project of one file, for testing a feature on `text`.
111    pub(crate) fn single(text: &str) -> Self {
112        Self::new(vec![Source {
113            uri: test_uri("main.nf"),
114            text: text.to_owned(),
115        }])
116    }
117
118    /// The cursor where `|` stands in `source`, in a project of that one file.
119    pub(crate) fn with_cursor(source: &str) -> (Self, usize) {
120        let offset = source
121            .find('|')
122            .expect("the source marks the cursor with `|`");
123        (Self::single(&source.replacen('|', "", 1)), offset)
124    }
125
126    pub(crate) fn cursor(&self, offset: usize) -> Cursor<'_> {
127        Cursor {
128            project: self,
129            file: FileId::default(),
130            offset,
131        }
132    }
133}
134
135#[cfg(test)]
136pub(crate) fn test_uri(name: &str) -> Uri {
137    format!("file:///project/{name}")
138        .parse()
139        .expect("a valid test uri")
140}