1 //! FIXME: write short doc here
8 use crossbeam_channel::{unbounded, Receiver};
9 use lsp_server::ErrorCode;
11 use parking_lot::RwLock;
13 Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData,
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};
23 main_loop::pending_requests::{CompletedRequest, LatestRequests},
26 use std::str::FromStr;
28 #[derive(Debug, Clone)]
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>,
36 /// `WorldState` is the primary mutable state of the language server
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.
42 pub struct WorldState {
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>>,
54 /// An immutable snapshot of the world's state at a point in time.
55 pub struct WorldSnapshot {
57 pub workspaces: Arc<Vec<ProjectWorkspace>>,
58 pub analysis: Analysis,
59 pub vfs: Arc<RwLock<Vfs>>,
60 pub latest_requests: Arc<RwLock<LatestRequests>>,
65 folder_roots: Vec<PathBuf>,
66 workspaces: Vec<ProjectWorkspace>,
67 lru_capacity: Option<usize>,
68 exclude_globs: &[Glob],
71 feature_flags: FeatureFlags,
73 let mut change = AnalysisChange::new();
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());
81 RootEntry::new(path.clone(), filter.into_vfs_filter())
83 for ws in workspaces.iter() {
84 roots.extend(ws.to_roots().into_iter().map(|pkg_root| {
86 RustPackageFilterBuilder::default().set_member(pkg_root.is_member());
87 for glob in exclude_globs.iter() {
88 filter = filter.exclude(glob.clone());
90 RootEntry::new(pkg_root.path().clone(), filter.into_vfs_filter())
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();
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());
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());
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))
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)
125 change.set_crate_graph(crate_graph);
127 let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags);
128 analysis_host.apply_change(change);
133 workspaces: Arc::new(workspaces),
135 vfs: Arc::new(RwLock::new(vfs)),
137 latest_requests: Default::default(),
141 /// Returns a vec of libraries
142 /// FIXME: better API here
143 pub fn process_changes(
145 ) -> Vec<(SourceRootId, Vec<(FileId, RelativePathBuf, Arc<String>)>)> {
146 let changes = self.vfs.write().commit_changes();
147 if changes.is_empty() {
150 let mut libs = Vec::new();
151 let mut change = AnalysisChange::new();
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));
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);
165 .map(|(vfsfile, path, text)| (FileId(vfsfile.0), path, text))
167 libs.push((SourceRootId(root.0), files));
170 VfsChange::AddFile { root, file, path, text } => {
171 change.add_file(SourceRootId(root.0), FileId(file.0), path, text);
173 VfsChange::RemoveFile { root, file, path } => {
174 change.remove_file(SourceRootId(root.0), FileId(file.0), path)
176 VfsChange::ChangeFile { file, text } => {
177 change.change_file(FileId(file.0), text);
181 self.analysis_host.apply_change(change);
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);
192 pub fn snapshot(&self) -> 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),
202 pub fn maybe_collect_garbage(&mut self) {
203 self.analysis_host.maybe_collect_garbage()
206 pub fn collect_garbage(&mut self) {
207 self.analysis_host.collect_garbage()
210 pub fn complete_request(&mut self, request: CompletedRequest) {
211 self.latest_requests.write().record(request)
214 pub fn feature_flags(&self) -> &FeatureFlags {
215 self.analysis_host.feature_flags()
220 pub fn analysis(&self) -> &Analysis {
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
229 code: ErrorCode::InvalidRequest as i32,
230 message: "Rust file outside current workspace is not supported yet.".to_string(),
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)?;
243 pub fn file_line_endings(&self, id: FileId) -> LineEndings {
244 self.vfs.read().file_line_endings(VfsFile(id.0))
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()))?;
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")
260 res.push_str("workspaces:\n");
261 for w in self.workspaces.iter() {
262 res += &format!("{} packages loaded\n", w.n_packages());
265 res.push_str("\nanalysis:\n");
270 .unwrap_or_else(|_| "Analysis retrieval was cancelled".to_owned()),
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))
280 pub fn feature_flags(&self) -> &FeatureFlags {
281 self.analysis.feature_flags()
285 /// Returns a `Url` object from a given path, will lowercase drive letters if present.
286 /// This will only happen when processing windows paths.
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
294 if let Component::Prefix(c) = comp {
296 Prefix::Disk(_) | Prefix::VerbatimDisk(_) => return true,
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()))?;
309 let drive_partition: Vec<&str> = url_original.as_str().rsplitn(2, ':').collect();
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);
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`");
322 Ok(Url::from_file_path(&path)
323 .map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?)
327 // `Url` is not able to parse windows paths on unix machines.
328 #[cfg(target_os = "windows")]
330 mod path_conversion_windows_tests {
331 use super::url_from_path_with_drive_lowercasing;
333 fn test_lowercase_drive_letter_with_drive() {
334 let url = url_from_path_with_drive_lowercasing("C:\\Test").unwrap();
336 assert_eq!(url.to_string(), "file:///c:/Test");
340 fn test_drive_without_colon_passthrough() {
341 let url = url_from_path_with_drive_lowercasing(r#"\\localhost\C$\my_dir"#).unwrap();
343 assert_eq!(url.to_string(), "file://localhost/C$/my_dir");