]> git.lizzy.rs Git - rust.git/blob - crates/ra_lsp_server/src/world.rs
16cc11e8cefa9923daae1d3623fd9ac2c454f877
[rust.git] / crates / ra_lsp_server / src / world.rs
1 //! FIXME: write short doc here
2
3 use std::{
4     path::{Path, PathBuf},
5     sync::Arc,
6 };
7
8 use crossbeam_channel::{unbounded, Receiver};
9 use lsp_server::ErrorCode;
10 use lsp_types::Url;
11 use parking_lot::RwLock;
12 use ra_ide::{
13     Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData,
14     SourceRootId,
15 };
16 use ra_project_model::{get_rustc_cfg_options, ProjectWorkspace};
17 use ra_vfs::{LineEndings, RootEntry, Vfs, VfsChange, VfsFile, VfsRoot, VfsTask, Watch};
18 use ra_vfs_glob::{Glob, RustPackageFilterBuilder};
19 use relative_path::RelativePathBuf;
20 use std::path::{Component, Prefix};
21
22 use crate::{
23     main_loop::pending_requests::{CompletedRequest, LatestRequests},
24     LspError, Result,
25 };
26 use std::str::FromStr;
27
28 #[derive(Debug, Clone)]
29 pub struct Options {
30     pub publish_decorations: bool,
31     pub supports_location_link: bool,
32     pub line_folding_only: bool,
33     pub max_inlay_hint_length: Option<usize>,
34 }
35
36 /// `WorldState` is the primary mutable state of the language server
37 ///
38 /// The most interesting components are `vfs`, which stores a consistent
39 /// snapshot of the file systems, and `analysis_host`, which stores our
40 /// incremental salsa database.
41 #[derive(Debug)]
42 pub struct WorldState {
43     pub options: Options,
44     //FIXME: this belongs to `LoopState` rather than to `WorldState`
45     pub roots_to_scan: usize,
46     pub roots: Vec<PathBuf>,
47     pub workspaces: Arc<Vec<ProjectWorkspace>>,
48     pub analysis_host: AnalysisHost,
49     pub vfs: Arc<RwLock<Vfs>>,
50     pub task_receiver: Receiver<VfsTask>,
51     pub latest_requests: Arc<RwLock<LatestRequests>>,
52 }
53
54 /// An immutable snapshot of the world's state at a point in time.
55 pub struct WorldSnapshot {
56     pub options: Options,
57     pub workspaces: Arc<Vec<ProjectWorkspace>>,
58     pub analysis: Analysis,
59     pub vfs: Arc<RwLock<Vfs>>,
60     pub latest_requests: Arc<RwLock<LatestRequests>>,
61 }
62
63 impl WorldState {
64     pub fn new(
65         folder_roots: Vec<PathBuf>,
66         workspaces: Vec<ProjectWorkspace>,
67         lru_capacity: Option<usize>,
68         exclude_globs: &[Glob],
69         watch: Watch,
70         options: Options,
71         feature_flags: FeatureFlags,
72     ) -> WorldState {
73         let mut change = AnalysisChange::new();
74
75         let mut roots = Vec::new();
76         roots.extend(folder_roots.iter().map(|path| {
77             let mut filter = RustPackageFilterBuilder::default().set_member(true);
78             for glob in exclude_globs.iter() {
79                 filter = filter.exclude(glob.clone());
80             }
81             RootEntry::new(path.clone(), filter.into_vfs_filter())
82         }));
83         for ws in workspaces.iter() {
84             roots.extend(ws.to_roots().into_iter().map(|pkg_root| {
85                 let mut filter =
86                     RustPackageFilterBuilder::default().set_member(pkg_root.is_member());
87                 for glob in exclude_globs.iter() {
88                     filter = filter.exclude(glob.clone());
89                 }
90                 RootEntry::new(pkg_root.path().clone(), filter.into_vfs_filter())
91             }));
92         }
93         let (task_sender, task_receiver) = unbounded();
94         let task_sender = Box::new(move |t| task_sender.send(t).unwrap());
95         let (mut vfs, vfs_roots) = Vfs::new(roots, task_sender, watch);
96         let roots_to_scan = vfs_roots.len();
97         for r in vfs_roots {
98             let vfs_root_path = vfs.root2path(r);
99             let is_local = folder_roots.iter().any(|it| vfs_root_path.starts_with(it));
100             change.add_root(SourceRootId(r.0), is_local);
101             change.set_debug_root_path(SourceRootId(r.0), vfs_root_path.display().to_string());
102         }
103
104         // FIXME: Read default cfgs from config
105         let default_cfg_options = {
106             let mut opts = get_rustc_cfg_options();
107             opts.insert_atom("test".into());
108             opts.insert_atom("debug_assertion".into());
109             opts
110         };
111
112         // Create crate graph from all the workspaces
113         let mut crate_graph = CrateGraph::default();
114         let mut load = |path: &std::path::Path| {
115             let vfs_file = vfs.load(path);
116             vfs_file.map(|f| FileId(f.0))
117         };
118         for ws in workspaces.iter() {
119             let (graph, crate_names) = ws.to_crate_graph(&default_cfg_options, &mut load);
120             let shift = crate_graph.extend(graph);
121             for (crate_id, name) in crate_names {
122                 change.set_debug_crate_name(crate_id.shift(shift), name)
123             }
124         }
125         change.set_crate_graph(crate_graph);
126
127         let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags);
128         analysis_host.apply_change(change);
129         WorldState {
130             options,
131             roots_to_scan,
132             roots: folder_roots,
133             workspaces: Arc::new(workspaces),
134             analysis_host,
135             vfs: Arc::new(RwLock::new(vfs)),
136             task_receiver,
137             latest_requests: Default::default(),
138         }
139     }
140
141     /// Returns a vec of libraries
142     /// FIXME: better API here
143     pub fn process_changes(
144         &mut self,
145     ) -> Vec<(SourceRootId, Vec<(FileId, RelativePathBuf, Arc<String>)>)> {
146         let changes = self.vfs.write().commit_changes();
147         if changes.is_empty() {
148             return Vec::new();
149         }
150         let mut libs = Vec::new();
151         let mut change = AnalysisChange::new();
152         for c in changes {
153             match c {
154                 VfsChange::AddRoot { root, files } => {
155                     let root_path = self.vfs.read().root2path(root);
156                     let is_local = self.roots.iter().any(|r| root_path.starts_with(r));
157                     if is_local {
158                         self.roots_to_scan -= 1;
159                         for (file, path, text) in files {
160                             change.add_file(SourceRootId(root.0), FileId(file.0), path, text);
161                         }
162                     } else {
163                         let files = files
164                             .into_iter()
165                             .map(|(vfsfile, path, text)| (FileId(vfsfile.0), path, text))
166                             .collect();
167                         libs.push((SourceRootId(root.0), files));
168                     }
169                 }
170                 VfsChange::AddFile { root, file, path, text } => {
171                     change.add_file(SourceRootId(root.0), FileId(file.0), path, text);
172                 }
173                 VfsChange::RemoveFile { root, file, path } => {
174                     change.remove_file(SourceRootId(root.0), FileId(file.0), path)
175                 }
176                 VfsChange::ChangeFile { file, text } => {
177                     change.change_file(FileId(file.0), text);
178                 }
179             }
180         }
181         self.analysis_host.apply_change(change);
182         libs
183     }
184
185     pub fn add_lib(&mut self, data: LibraryData) {
186         self.roots_to_scan -= 1;
187         let mut change = AnalysisChange::new();
188         change.add_library(data);
189         self.analysis_host.apply_change(change);
190     }
191
192     pub fn snapshot(&self) -> WorldSnapshot {
193         WorldSnapshot {
194             options: self.options.clone(),
195             workspaces: Arc::clone(&self.workspaces),
196             analysis: self.analysis_host.analysis(),
197             vfs: Arc::clone(&self.vfs),
198             latest_requests: Arc::clone(&self.latest_requests),
199         }
200     }
201
202     pub fn maybe_collect_garbage(&mut self) {
203         self.analysis_host.maybe_collect_garbage()
204     }
205
206     pub fn collect_garbage(&mut self) {
207         self.analysis_host.collect_garbage()
208     }
209
210     pub fn complete_request(&mut self, request: CompletedRequest) {
211         self.latest_requests.write().record(request)
212     }
213
214     pub fn feature_flags(&self) -> &FeatureFlags {
215         self.analysis_host.feature_flags()
216     }
217 }
218
219 impl WorldSnapshot {
220     pub fn analysis(&self) -> &Analysis {
221         &self.analysis
222     }
223
224     pub fn uri_to_file_id(&self, uri: &Url) -> Result<FileId> {
225         let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
226         let file = self.vfs.read().path2file(&path).ok_or_else(|| {
227             // Show warning as this file is outside current workspace
228             LspError {
229                 code: ErrorCode::InvalidRequest as i32,
230                 message: "Rust file outside current workspace is not supported yet.".to_string(),
231             }
232         })?;
233         Ok(FileId(file.0))
234     }
235
236     pub fn file_id_to_uri(&self, id: FileId) -> Result<Url> {
237         let path = self.vfs.read().file2path(VfsFile(id.0));
238         let url = url_from_path_with_drive_lowercasing(path)?;
239
240         Ok(url)
241     }
242
243     pub fn file_line_endings(&self, id: FileId) -> LineEndings {
244         self.vfs.read().file_line_endings(VfsFile(id.0))
245     }
246
247     pub fn path_to_uri(&self, root: SourceRootId, path: &RelativePathBuf) -> Result<Url> {
248         let base = self.vfs.read().root2path(VfsRoot(root.0));
249         let path = path.to_path(base);
250         let url = Url::from_file_path(&path)
251             .map_err(|_| format!("can't convert path to url: {}", path.display()))?;
252         Ok(url)
253     }
254
255     pub fn status(&self) -> String {
256         let mut res = String::new();
257         if self.workspaces.is_empty() {
258             res.push_str("no workspaces\n")
259         } else {
260             res.push_str("workspaces:\n");
261             for w in self.workspaces.iter() {
262                 res += &format!("{} packages loaded\n", w.n_packages());
263             }
264         }
265         res.push_str("\nanalysis:\n");
266         res.push_str(
267             &self
268                 .analysis
269                 .status()
270                 .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
271         );
272         res
273     }
274
275     pub fn workspace_root_for(&self, file_id: FileId) -> Option<&Path> {
276         let path = self.vfs.read().file2path(VfsFile(file_id.0));
277         self.workspaces.iter().find_map(|ws| ws.workspace_root_for(&path))
278     }
279
280     pub fn feature_flags(&self) -> &FeatureFlags {
281         self.analysis.feature_flags()
282     }
283 }
284
285 /// Returns a `Url` object from a given path, will lowercase drive letters if present.
286 /// This will only happen when processing windows paths.
287 ///
288 /// When processing non-windows path, this is essentially the same as `Url::from_file_path`.
289 fn url_from_path_with_drive_lowercasing(path: impl AsRef<Path>) -> Result<Url> {
290     let component_has_windows_drive = path
291         .as_ref()
292         .components()
293         .find(|comp| {
294             if let Component::Prefix(c) = comp {
295                 match c.kind() {
296                     Prefix::Disk(_) | Prefix::VerbatimDisk(_) => return true,
297                     _ => return false,
298                 }
299             }
300             false
301         })
302         .is_some();
303
304     // VSCode expects drive letters to be lowercased, where rust will uppercase the drive letters.
305     if component_has_windows_drive {
306         let url_original = Url::from_file_path(&path)
307             .map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?;
308
309         let drive_partition: Vec<&str> = url_original.as_str().rsplitn(2, ':').collect();
310
311         // There is a drive partition, but we never found a colon.
312         // This should not happen, but in this case we just pass it through.
313         if drive_partition.len() == 1 {
314             return Ok(url_original);
315         }
316
317         let joined = drive_partition[1].to_ascii_lowercase() + ":" + drive_partition[0];
318         let url = Url::from_str(&joined).expect("This came from a valid `Url`");
319
320         Ok(url)
321     } else {
322         Ok(Url::from_file_path(&path)
323             .map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?)
324     }
325 }
326
327 // `Url` is not able to parse windows paths on unix machines.
328 #[cfg(target_os = "windows")]
329 #[cfg(test)]
330 mod path_conversion_windows_tests {
331     use super::url_from_path_with_drive_lowercasing;
332     #[test]
333     fn test_lowercase_drive_letter_with_drive() {
334         let url = url_from_path_with_drive_lowercasing("C:\\Test").unwrap();
335
336         assert_eq!(url.to_string(), "file:///c:/Test");
337     }
338
339     #[test]
340     fn test_drive_without_colon_passthrough() {
341         let url = url_from_path_with_drive_lowercasing(r#"\\localhost\C$\my_dir"#).unwrap();
342
343         assert_eq!(url.to_string(), "file://localhost/C$/my_dir");
344     }
345 }