]> git.lizzy.rs Git - rust.git/blob - crates/rust-analyzer/src/reload.rs
internal: simplify handling of the build scripts
[rust.git] / crates / rust-analyzer / src / reload.rs
1 //! Project loading & configuration updates
2 use std::{mem, sync::Arc};
3
4 use always_assert::always;
5 use flycheck::{FlycheckConfig, FlycheckHandle};
6 use hir::db::DefDatabase;
7 use ide::Change;
8 use ide_db::base_db::{CrateGraph, SourceRoot, VfsPath};
9 use project_model::{ProcMacroClient, ProjectWorkspace, WorkspaceBuildScripts};
10 use vfs::{file_set::FileSetConfig, AbsPath, AbsPathBuf, ChangeKind};
11
12 use crate::{
13     config::{Config, FilesWatcher, LinkedProject},
14     global_state::GlobalState,
15     lsp_ext,
16     main_loop::Task,
17 };
18
19 #[derive(Debug)]
20 pub(crate) enum ProjectWorkspaceProgress {
21     Begin,
22     Report(String),
23     End(Vec<anyhow::Result<ProjectWorkspace>>),
24 }
25
26 #[derive(Debug)]
27 pub(crate) enum BuildDataProgress {
28     Begin,
29     Report(String),
30     End(Vec<anyhow::Result<WorkspaceBuildScripts>>),
31 }
32
33 impl GlobalState {
34     pub(crate) fn is_quiescent(&self) -> bool {
35         !(self.fetch_workspaces_queue.op_in_progress()
36             || self.fetch_build_data_queue.op_in_progress()
37             || self.vfs_progress_config_version < self.vfs_config_version
38             || self.vfs_progress_n_done < self.vfs_progress_n_total)
39     }
40
41     pub(crate) fn update_configuration(&mut self, config: Config) {
42         let _p = profile::span("GlobalState::update_configuration");
43         let old_config = mem::replace(&mut self.config, Arc::new(config));
44         if self.config.lru_capacity() != old_config.lru_capacity() {
45             self.analysis_host.update_lru_capacity(self.config.lru_capacity());
46         }
47         if self.config.linked_projects() != old_config.linked_projects() {
48             self.fetch_workspaces_request()
49         } else if self.config.flycheck() != old_config.flycheck() {
50             self.reload_flycheck();
51         }
52
53         // Apply experimental feature flags.
54         self.analysis_host
55             .raw_database_mut()
56             .set_enable_proc_attr_macros(self.config.expand_proc_attr_macros());
57     }
58     pub(crate) fn maybe_refresh(&mut self, changes: &[(AbsPathBuf, ChangeKind)]) {
59         if !changes.iter().any(|(path, kind)| is_interesting(path, *kind)) {
60             return;
61         }
62         log::info!(
63             "Requesting workspace reload because of the following changes: {}",
64             itertools::join(
65                 changes
66                     .iter()
67                     .filter(|(path, kind)| is_interesting(path, *kind))
68                     .map(|(path, kind)| format!("{}: {:?}", path.display(), kind)),
69                 ", "
70             )
71         );
72         self.fetch_workspaces_request();
73
74         fn is_interesting(path: &AbsPath, change_kind: ChangeKind) -> bool {
75             const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
76             const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];
77             let file_name = path.file_name().unwrap_or_default();
78
79             if file_name == "Cargo.toml" || file_name == "Cargo.lock" {
80                 return true;
81             }
82             if change_kind == ChangeKind::Modify {
83                 return false;
84             }
85             if path.extension().unwrap_or_default() != "rs" {
86                 return false;
87             }
88             if IMPLICIT_TARGET_FILES.iter().any(|it| path.as_ref().ends_with(it)) {
89                 return true;
90             }
91             let parent = match path.parent() {
92                 Some(it) => it,
93                 None => return false,
94             };
95             if IMPLICIT_TARGET_DIRS.iter().any(|it| parent.as_ref().ends_with(it)) {
96                 return true;
97             }
98             if file_name == "main.rs" {
99                 let grand_parent = match parent.parent() {
100                     Some(it) => it,
101                     None => return false,
102                 };
103                 if IMPLICIT_TARGET_DIRS.iter().any(|it| grand_parent.as_ref().ends_with(it)) {
104                     return true;
105                 }
106             }
107             false
108         }
109     }
110     pub(crate) fn report_new_status_if_needed(&mut self) {
111         let mut status = lsp_ext::ServerStatusParams {
112             health: lsp_ext::Health::Ok,
113             quiescent: self.is_quiescent(),
114             message: None,
115         };
116
117         if let Some(error) = self.build_data_error() {
118             status.health = lsp_ext::Health::Warning;
119             status.message = Some(error)
120         }
121         if !self.config.cargo_autoreload()
122             && self.is_quiescent()
123             && self.fetch_workspaces_queue.op_requested()
124         {
125             status.health = lsp_ext::Health::Warning;
126             status.message = Some("Workspace reload required".to_string())
127         }
128
129         if let Some(error) = self.fetch_workspace_error() {
130             status.health = lsp_ext::Health::Error;
131             status.message = Some(error)
132         }
133
134         if self.last_reported_status.as_ref() != Some(&status) {
135             self.last_reported_status = Some(status.clone());
136
137             if let (lsp_ext::Health::Error, Some(message)) = (status.health, &status.message) {
138                 self.show_message(lsp_types::MessageType::Error, message.clone());
139             }
140
141             if self.config.server_status_notification() {
142                 self.send_notification::<lsp_ext::ServerStatusNotification>(status);
143             }
144         }
145     }
146
147     pub(crate) fn fetch_workspaces_request(&mut self) {
148         self.fetch_workspaces_queue.request_op()
149     }
150     pub(crate) fn fetch_workspaces_if_needed(&mut self) {
151         if !self.fetch_workspaces_queue.should_start_op() {
152             return;
153         }
154         log::info!("will fetch workspaces");
155
156         self.task_pool.handle.spawn_with_sender({
157             let linked_projects = self.config.linked_projects();
158             let detached_files = self.config.detached_files().to_vec();
159             let cargo_config = self.config.cargo();
160
161             move |sender| {
162                 let progress = {
163                     let sender = sender.clone();
164                     move |msg| {
165                         sender
166                             .send(Task::FetchWorkspace(ProjectWorkspaceProgress::Report(msg)))
167                             .unwrap()
168                     }
169                 };
170
171                 sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();
172
173                 let mut workspaces = linked_projects
174                     .iter()
175                     .map(|project| match project {
176                         LinkedProject::ProjectManifest(manifest) => {
177                             project_model::ProjectWorkspace::load(
178                                 manifest.clone(),
179                                 &cargo_config,
180                                 &progress,
181                             )
182                         }
183                         LinkedProject::InlineJsonProject(it) => {
184                             project_model::ProjectWorkspace::load_inline(
185                                 it.clone(),
186                                 cargo_config.target.as_deref(),
187                             )
188                         }
189                     })
190                     .collect::<Vec<_>>();
191
192                 if !detached_files.is_empty() {
193                     workspaces
194                         .push(project_model::ProjectWorkspace::load_detached_files(detached_files));
195                 }
196
197                 log::info!("did fetch workspaces {:?}", workspaces);
198                 sender
199                     .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(workspaces)))
200                     .unwrap();
201             }
202         });
203     }
204     pub(crate) fn fetch_workspaces_completed(
205         &mut self,
206         workspaces: Vec<anyhow::Result<ProjectWorkspace>>,
207     ) {
208         self.fetch_workspaces_queue.op_completed(workspaces)
209     }
210
211     pub(crate) fn fetch_build_data_request(&mut self) {
212         self.fetch_build_data_queue.request_op();
213     }
214     pub(crate) fn fetch_build_data_if_needed(&mut self) {
215         if !self.fetch_build_data_queue.should_start_op() {
216             return;
217         }
218
219         let workspaces = Arc::clone(&self.workspaces);
220         let config = self.config.cargo();
221         self.task_pool.handle.spawn_with_sender(move |sender| {
222             sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap();
223
224             let progress = {
225                 let sender = sender.clone();
226                 move |msg| {
227                     sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
228                 }
229             };
230             let mut res = Vec::new();
231             for ws in workspaces.iter() {
232                 let ws = match ws {
233                     ProjectWorkspace::Cargo { cargo, .. } => cargo,
234                     ProjectWorkspace::DetachedFiles { .. } | ProjectWorkspace::Json { .. } => {
235                         res.push(Ok(WorkspaceBuildScripts::default()));
236                         continue;
237                     }
238                 };
239                 res.push(WorkspaceBuildScripts::run(&config, ws, &progress))
240             }
241             sender.send(Task::FetchBuildData(BuildDataProgress::End(res))).unwrap();
242         });
243     }
244     pub(crate) fn fetch_build_data_completed(
245         &mut self,
246         build_data: Vec<anyhow::Result<WorkspaceBuildScripts>>,
247     ) {
248         self.fetch_build_data_queue.op_completed(build_data)
249     }
250
251     pub(crate) fn switch_workspaces(&mut self) {
252         let _p = profile::span("GlobalState::switch_workspaces");
253         log::info!("will switch workspaces");
254
255         if let Some(error_message) = self.fetch_workspace_error() {
256             log::error!("failed to switch workspaces: {}", error_message);
257             if !self.workspaces.is_empty() {
258                 return;
259             }
260         }
261
262         if let Some(error_message) = self.build_data_error() {
263             log::error!("failed to switch build data: {}", error_message);
264         }
265
266         let workspaces = self
267             .fetch_workspaces_queue
268             .last_op_result()
269             .iter()
270             .filter_map(|res| res.as_ref().ok().cloned())
271             .collect::<Vec<_>>();
272
273         let mut build_scripts = self
274             .fetch_build_data_queue
275             .last_op_result()
276             .iter()
277             .map(|res| res.as_ref().ok().cloned().unwrap_or_default())
278             .collect::<Vec<_>>();
279
280         // FIXME: This is not even remotely correct. I do hope that this is
281         // eventually consistent though. We need to figure a better way to map
282         // `cargo metadata` to `cargo check` in the future.
283         //
284         // I *think* what we need here is an extra field on `ProjectWorkspace`,
285         // and a workflow to set it, once build data is ready.
286         build_scripts.resize_with(workspaces.len(), WorkspaceBuildScripts::default);
287
288         if *self.workspaces == workspaces && self.workspace_build_data == build_scripts {
289             return;
290         }
291
292         if let FilesWatcher::Client = self.config.files().watcher {
293             if self.config.did_change_watched_files_dynamic_registration() {
294                 let registration_options = lsp_types::DidChangeWatchedFilesRegistrationOptions {
295                     watchers: workspaces
296                         .iter()
297                         .zip(&build_scripts)
298                         .flat_map(|(ws, bs)| ws.to_roots(bs))
299                         .filter(|it| it.is_member)
300                         .flat_map(|root| {
301                             root.include.into_iter().flat_map(|it| {
302                                 [
303                                     format!("{}/**/*.rs", it.display()),
304                                     format!("{}/**/Cargo.toml", it.display()),
305                                     format!("{}/**/Cargo.lock", it.display()),
306                                 ]
307                             })
308                         })
309                         .map(|glob_pattern| lsp_types::FileSystemWatcher {
310                             glob_pattern,
311                             kind: None,
312                         })
313                         .collect(),
314                 };
315                 let registration = lsp_types::Registration {
316                     id: "workspace/didChangeWatchedFiles".to_string(),
317                     method: "workspace/didChangeWatchedFiles".to_string(),
318                     register_options: Some(serde_json::to_value(registration_options).unwrap()),
319                 };
320                 self.send_request::<lsp_types::request::RegisterCapability>(
321                     lsp_types::RegistrationParams { registrations: vec![registration] },
322                     |_, _| (),
323                 );
324             }
325         }
326
327         let mut change = Change::new();
328
329         let files_config = self.config.files();
330         let project_folders =
331             ProjectFolders::new(&workspaces, &build_scripts, &files_config.exclude);
332
333         if self.proc_macro_client.is_none() {
334             self.proc_macro_client = match self.config.proc_macro_srv() {
335                 None => None,
336                 Some((path, args)) => match ProcMacroClient::extern_process(path.clone(), args) {
337                     Ok(it) => Some(it),
338                     Err(err) => {
339                         log::error!(
340                             "Failed to run proc_macro_srv from path {}, error: {:?}",
341                             path.display(),
342                             err
343                         );
344                         None
345                     }
346                 },
347             };
348         }
349
350         let watch = match files_config.watcher {
351             FilesWatcher::Client => vec![],
352             FilesWatcher::Notify => project_folders.watch,
353         };
354         self.vfs_config_version += 1;
355         self.loader.handle.set_config(vfs::loader::Config {
356             load: project_folders.load,
357             watch,
358             version: self.vfs_config_version,
359         });
360
361         // Create crate graph from all the workspaces
362         let crate_graph = {
363             let mut crate_graph = CrateGraph::default();
364             let vfs = &mut self.vfs.write().0;
365             let loader = &mut self.loader;
366             let mem_docs = &self.mem_docs;
367             let mut load = |path: &AbsPath| {
368                 let _p = profile::span("GlobalState::load");
369                 let vfs_path = vfs::VfsPath::from(path.to_path_buf());
370                 if !mem_docs.contains_key(&vfs_path) {
371                     let contents = loader.handle.load_sync(path);
372                     vfs.set_file_contents(vfs_path.clone(), contents);
373                 }
374                 let res = vfs.file_id(&vfs_path);
375                 if res.is_none() {
376                     log::warn!("failed to load {}", path.display())
377                 }
378                 res
379             };
380             for (ws, bs) in workspaces.iter().zip(&build_scripts) {
381                 crate_graph.extend(ws.to_crate_graph(
382                     bs,
383                     self.proc_macro_client.as_ref(),
384                     &mut load,
385                 ));
386             }
387
388             crate_graph
389         };
390         change.set_crate_graph(crate_graph);
391
392         self.source_root_config = project_folders.source_root_config;
393         self.workspaces = Arc::new(workspaces);
394         self.workspace_build_data = build_scripts;
395
396         self.analysis_host.apply_change(change);
397         self.process_changes();
398         self.reload_flycheck();
399         log::info!("did switch workspaces");
400     }
401
402     fn fetch_workspace_error(&self) -> Option<String> {
403         let mut buf = String::new();
404
405         for ws in self.fetch_workspaces_queue.last_op_result() {
406             if let Err(err) = ws {
407                 stdx::format_to!(buf, "rust-analyzer failed to load workspace: {:#}\n", err);
408             }
409         }
410
411         if buf.is_empty() {
412             return None;
413         }
414
415         Some(buf)
416     }
417
418     fn build_data_error(&self) -> Option<String> {
419         let mut buf = String::new();
420
421         for ws in self.fetch_build_data_queue.last_op_result() {
422             if let Err(err) = ws {
423                 stdx::format_to!(buf, "rust-analyzer failed to run custom build: {:#}\n", err);
424             }
425         }
426
427         if buf.is_empty() {
428             return None;
429         }
430
431         Some(buf)
432     }
433
434     fn reload_flycheck(&mut self) {
435         let _p = profile::span("GlobalState::reload_flycheck");
436         let config = match self.config.flycheck() {
437             Some(it) => it,
438             None => {
439                 self.flycheck = Vec::new();
440                 return;
441             }
442         };
443
444         let sender = self.flycheck_sender.clone();
445         self.flycheck = self
446             .workspaces
447             .iter()
448             .enumerate()
449             .filter_map(|(id, w)| match w {
450                 ProjectWorkspace::Cargo { cargo, .. } => Some((id, cargo.workspace_root())),
451                 ProjectWorkspace::Json { project, .. } => {
452                     // Enable flychecks for json projects if a custom flycheck command was supplied
453                     // in the workspace configuration.
454                     match config {
455                         FlycheckConfig::CustomCommand { .. } => Some((id, project.path())),
456                         _ => None,
457                     }
458                 }
459                 ProjectWorkspace::DetachedFiles { .. } => None,
460             })
461             .map(|(id, root)| {
462                 let sender = sender.clone();
463                 FlycheckHandle::spawn(
464                     id,
465                     Box::new(move |msg| sender.send(msg).unwrap()),
466                     config.clone(),
467                     root.to_path_buf().into(),
468                 )
469             })
470             .collect();
471     }
472 }
473
474 #[derive(Default)]
475 pub(crate) struct ProjectFolders {
476     pub(crate) load: Vec<vfs::loader::Entry>,
477     pub(crate) watch: Vec<usize>,
478     pub(crate) source_root_config: SourceRootConfig,
479 }
480
481 impl ProjectFolders {
482     pub(crate) fn new(
483         workspaces: &[ProjectWorkspace],
484         build_scripts: &[WorkspaceBuildScripts],
485         global_excludes: &[AbsPathBuf],
486     ) -> ProjectFolders {
487         always!(workspaces.len() == build_scripts.len());
488         let mut res = ProjectFolders::default();
489         let mut fsc = FileSetConfig::builder();
490         let mut local_filesets = vec![];
491
492         for root in workspaces.iter().zip(build_scripts).flat_map(|(ws, bs)| ws.to_roots(bs)) {
493             let file_set_roots: Vec<VfsPath> =
494                 root.include.iter().cloned().map(VfsPath::from).collect();
495
496             let entry = {
497                 let mut dirs = vfs::loader::Directories::default();
498                 dirs.extensions.push("rs".into());
499                 dirs.include.extend(root.include);
500                 dirs.exclude.extend(root.exclude);
501                 for excl in global_excludes {
502                     if dirs
503                         .include
504                         .iter()
505                         .any(|incl| incl.starts_with(excl) || excl.starts_with(incl))
506                     {
507                         dirs.exclude.push(excl.clone());
508                     }
509                 }
510
511                 vfs::loader::Entry::Directories(dirs)
512             };
513
514             if root.is_member {
515                 res.watch.push(res.load.len());
516             }
517             res.load.push(entry);
518
519             if root.is_member {
520                 local_filesets.push(fsc.len());
521             }
522             fsc.add_file_set(file_set_roots)
523         }
524
525         let fsc = fsc.build();
526         res.source_root_config = SourceRootConfig { fsc, local_filesets };
527
528         res
529     }
530 }
531
532 #[derive(Default, Debug)]
533 pub(crate) struct SourceRootConfig {
534     pub(crate) fsc: FileSetConfig,
535     pub(crate) local_filesets: Vec<usize>,
536 }
537
538 impl SourceRootConfig {
539     pub(crate) fn partition(&self, vfs: &vfs::Vfs) -> Vec<SourceRoot> {
540         let _p = profile::span("SourceRootConfig::partition");
541         self.fsc
542             .partition(vfs)
543             .into_iter()
544             .enumerate()
545             .map(|(idx, file_set)| {
546                 let is_local = self.local_filesets.contains(&idx);
547                 if is_local {
548                     SourceRoot::new_local(file_set)
549                 } else {
550                     SourceRoot::new_library(file_set)
551                 }
552             })
553             .collect()
554     }
555 }