1use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use lsp_types::{PublishDiagnosticsParams, Uri};
10use nifty_syntax::FileId;
11use nifty_syntax::project::{self, root_of};
12use url::Url;
13
14use crate::document::Encoding;
15use crate::features::diagnostics;
16use crate::project::{Cursor, Project, Source};
17
18#[derive(Debug)]
19struct OpenDocument {
20 text: String,
21 version: i32,
22 path: Option<PathBuf>,
25}
26
27#[derive(Debug, Default)]
29pub(crate) struct Refreshed {
30 pub diagnostics: Vec<PublishDiagnosticsParams>,
33 pub warnings: Vec<String>,
35}
36
37#[derive(Debug, Default)]
38pub(crate) struct Workspace {
39 open: HashMap<Uri, OpenDocument>,
40 projects: Vec<Project>,
41}
42
43impl Workspace {
44 pub(crate) fn update(
45 &mut self,
46 uri: &Uri,
47 text: String,
48 version: i32,
49 encoding: Encoding,
50 ) -> Refreshed {
51 let path = path_of(uri);
52 self.open.insert(
53 uri.clone(),
54 OpenDocument {
55 text,
56 version,
57 path,
58 },
59 );
60 self.refresh(uri, encoding)
61 }
62
63 pub(crate) fn close(&mut self, uri: &Uri, encoding: Encoding) -> Refreshed {
64 self.open.remove(uri);
65 self.refresh(uri, encoding)
66 }
67
68 pub(crate) fn locate(
70 &self,
71 uri: &Uri,
72 position: lsp_types::Position,
73 encoding: Encoding,
74 ) -> Option<Cursor<'_>> {
75 let (project, file) = self.file(uri)?;
76 let offset = project.document(file).offset(position, encoding);
77 Some(Cursor {
78 project,
79 file,
80 offset,
81 })
82 }
83
84 pub(crate) fn file(&self, uri: &Uri) -> Option<(&Project, FileId)> {
86 self.projects
87 .iter()
88 .find_map(|project| Some((project, project.file_of(uri)?)))
89 }
90
91 fn refresh(&mut self, uri: &Uri, encoding: Encoding) -> Refreshed {
94 let old = self
95 .projects
96 .iter()
97 .position(|project| project.file_of(uri).is_some())
98 .map(|index| self.projects.swap_remove(index));
99 let still_open = if self.open.contains_key(uri) {
100 Some(uri.clone())
101 } else {
102 old.as_ref().and_then(|project| {
103 project
104 .files()
105 .map(|(_, uri)| uri)
106 .find(|uri| self.open.contains_key(uri))
107 .cloned()
108 })
109 };
110
111 let mut refreshed = Refreshed::default();
112 let new = still_open.map(|uri| Project::new(self.sources(&uri, &mut refreshed.warnings)));
113 if let Some(old) = &old {
114 for (_, uri) in old.files() {
115 if new.as_ref().is_none_or(|new| new.file_of(uri).is_none()) {
116 refreshed.diagnostics.push(self.publish(uri, Vec::new()));
117 }
118 }
119 }
120 if let Some(new) = new {
121 for (file, uri) in new.files() {
122 let found = diagnostics::diagnostics(&new, file, encoding);
123 refreshed.diagnostics.push(self.publish(uri, found));
124 }
125 self.projects.push(new);
126 }
127 refreshed
128 }
129
130 fn sources(&self, uri: &Uri, warnings: &mut Vec<String>) -> Vec<Source> {
133 let alone = |document: &OpenDocument| {
134 vec![Source {
135 uri: uri.clone(),
136 text: document.text.clone(),
137 }]
138 };
139 let Some(document) = self.open.get(uri) else {
140 return Vec::new();
141 };
142 let Some(root) = document.path.as_deref().and_then(root_of) else {
143 return alone(document);
144 };
145 let listed = match project::source_files(root) {
146 Ok(listed) => listed,
147 Err(error) => {
148 warnings.push(format!(
149 "can't list the files of {}: {error}",
150 root.display()
151 ));
152 return alone(document);
153 }
154 };
155 let mut sources: Vec<Source> = listed
156 .iter()
157 .filter_map(|relative| self.source(&root.join(relative), warnings))
158 .collect();
159 if !sources.iter().any(|source| source.uri == *uri) {
160 sources.extend(alone(document));
161 }
162 sources
163 }
164
165 fn source(&self, path: &Path, warnings: &mut Vec<String>) -> Option<Source> {
167 let open = self
168 .open
169 .iter()
170 .find(|(_, document)| document.path.as_deref() == Some(path));
171 if let Some((uri, document)) = open {
172 return Some(Source {
173 uri: uri.clone(),
174 text: document.text.clone(),
175 });
176 }
177 let Some(uri) = uri_of(path) else {
178 warnings.push(format!("can't address {} as a URI", path.display()));
179 return None;
180 };
181 match fs::read_to_string(path) {
182 Ok(text) => Some(Source { uri, text }),
183 Err(error) => {
184 warnings.push(format!("can't read {}: {error}", path.display()));
185 None
186 }
187 }
188 }
189
190 fn publish(
191 &self,
192 uri: &Uri,
193 diagnostics: Vec<lsp_types::Diagnostic>,
194 ) -> PublishDiagnosticsParams {
195 PublishDiagnosticsParams {
196 uri: uri.clone(),
197 diagnostics,
198 version: self.open.get(uri).map(|document| document.version),
199 }
200 }
201}
202
203fn path_of(uri: &Uri) -> Option<PathBuf> {
205 Url::parse(uri.as_str()).ok()?.to_file_path().ok()
206}
207
208fn uri_of(path: &Path) -> Option<Uri> {
209 Url::from_file_path(path).ok()?.as_str().parse().ok()
210}