Skip to main content

rust_analyzer/
reload.rs

1//! Project loading & configuration updates.
2//!
3//! This is quite tricky. The main problem is time and changes -- there's no
4//! fixed "project" rust-analyzer is working with, "current project" is itself
5//! mutable state. For example, when the user edits `Cargo.toml` by adding a new
6//! dependency, project model changes. What's more, switching project model is
7//! not instantaneous -- it takes time to run `cargo metadata` and (for proc
8//! macros) `cargo check`.
9//!
10//! The main guiding principle here is, as elsewhere in rust-analyzer,
11//! robustness. We try not to assume that the project model exists or is
12//! correct. Instead, we try to provide a best-effort service. Even if the
13//! project is currently loading and we don't have a full project model, we
14//! still want to respond to various  requests.
15// FIXME: This is a mess that needs some untangling work
16use std::{iter, mem, sync::atomic::AtomicUsize};
17
18use hir::{ChangeWithProcMacros, ProcMacrosBuilder, db::DefDatabase};
19use ide_db::{
20    FxHashMap,
21    base_db::{CrateGraphBuilder, ProcMacroLoadingError, ProcMacroPaths, salsa::Durability},
22};
23use itertools::Itertools;
24use load_cargo::{ProjectFolders, load_proc_macro};
25use lsp_types::FileSystemWatcher;
26use paths::Utf8Path;
27use proc_macro_api::ProcMacroClient;
28use project_model::{
29    ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts, project_json,
30};
31use stdx::{format_to, thread::ThreadIntent};
32use triomphe::Arc;
33use vfs::{AbsPath, AbsPathBuf, ChangeKind};
34
35use crate::{
36    config::{Config, FilesWatcher, LinkedProject},
37    flycheck::{FlycheckConfig, FlycheckHandle},
38    global_state::{
39        FetchBuildDataResponse, FetchWorkspaceRequest, FetchWorkspaceResponse, GlobalState,
40    },
41    lsp_ext,
42    main_loop::{DiscoverProjectParam, Task},
43    op_queue::Cause,
44};
45use tracing::{debug, info};
46
47#[derive(Debug)]
48pub(crate) enum ProjectWorkspaceProgress {
49    Begin,
50    Report(String),
51    End(Vec<anyhow::Result<ProjectWorkspace>>, bool),
52}
53
54#[derive(Debug)]
55pub(crate) enum BuildDataProgress {
56    Begin,
57    Report(String),
58    End((Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)),
59}
60
61#[derive(Debug)]
62pub(crate) enum ProcMacroProgress {
63    Begin,
64    Report(String),
65    End(ChangeWithProcMacros),
66}
67
68impl GlobalState {
69    /// Is the server quiescent?
70    ///
71    /// This indicates that we've fully loaded the projects and
72    /// are ready to do semantic work.
73    pub(crate) fn is_quiescent(&self) -> bool {
74        self.vfs_done
75            && self.fetch_ws_receiver.is_none()
76            && !self.fetch_workspaces_queue.op_in_progress()
77            && !self.fetch_build_data_queue.op_in_progress()
78            && !self.fetch_proc_macros_queue.op_in_progress()
79            && self.discover_jobs_active == 0
80            && self.vfs_progress_config_version >= self.vfs_config_version
81    }
82
83    /// Is the server ready to respond to analysis dependent LSP requests?
84    ///
85    /// Unlike `is_quiescent`, this returns false when we're indexing
86    /// the project, because we're holding the salsa lock and cannot
87    /// respond to LSP requests that depend on salsa data.
88    fn is_fully_ready(&self) -> bool {
89        self.is_quiescent() && !self.prime_caches_queue.op_in_progress()
90    }
91
92    pub(crate) fn update_configuration(&mut self, config: Config) {
93        let _p = tracing::info_span!("GlobalState::update_configuration").entered();
94        let old_config = mem::replace(&mut self.config, Arc::new(config));
95        if self.config.lru_parse_query_capacity() != old_config.lru_parse_query_capacity() {
96            self.analysis_host.update_lru_capacity(self.config.lru_parse_query_capacity());
97        }
98        if self.config.lru_query_capacities_config() != old_config.lru_query_capacities_config() {
99            self.analysis_host.update_lru_capacities(
100                &self.config.lru_query_capacities_config().cloned().unwrap_or_default(),
101            );
102        }
103
104        if self.config.linked_or_discovered_projects() != old_config.linked_or_discovered_projects()
105        {
106            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
107            self.fetch_workspaces_queue.request_op("discovered projects changed".to_owned(), req)
108        } else if self.config.flycheck(None) != old_config.flycheck(None) {
109            self.reload_flycheck();
110        }
111
112        if self.analysis_host.raw_database().expand_proc_attr_macros()
113            != self.config.expand_proc_attr_macros()
114        {
115            self.analysis_host.raw_database_mut().set_expand_proc_attr_macros_with_durability(
116                self.config.expand_proc_attr_macros(),
117                Durability::HIGH,
118            );
119        }
120
121        if self.config.cargo(None) != old_config.cargo(None) {
122            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
123            self.fetch_workspaces_queue.request_op("cargo config changed".to_owned(), req)
124        }
125
126        if self.config.cfg_set_test(None) != old_config.cfg_set_test(None) {
127            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
128            self.fetch_workspaces_queue.request_op("cfg_set_test config changed".to_owned(), req)
129        }
130    }
131
132    pub(crate) fn current_status(&self) -> lsp_ext::ServerStatusParams {
133        let mut status = lsp_ext::ServerStatusParams {
134            health: lsp_ext::Health::Ok,
135            quiescent: self.is_fully_ready(),
136            message: None,
137        };
138        let mut message = String::new();
139
140        if !self.config.cargo_autoreload_config(None)
141            && self.is_quiescent()
142            && self.fetch_workspaces_queue.op_requested()
143            && self.config.discover_workspace_config().is_none()
144        {
145            status.health |= lsp_ext::Health::Warning;
146            message.push_str("Auto-reloading is disabled and the workspace has changed, a manual workspace reload is required.\n\n");
147        }
148
149        if self.build_deps_changed {
150            status.health |= lsp_ext::Health::Warning;
151            message.push_str(
152                "Proc-macros and/or build scripts have changed and need to be rebuilt.\n\n",
153            );
154        }
155        if self.fetch_build_data_error().is_err() {
156            status.health |= lsp_ext::Health::Warning;
157            message.push_str("Failed to run build scripts of some packages.\n\n");
158            message.push_str(
159                "Please refer to the language server logs for more details on the errors.",
160            );
161        }
162        if let Some(err) = &self.config_errors {
163            status.health |= lsp_ext::Health::Warning;
164            format_to!(message, "{err}\n");
165        }
166        if let Some(err) = &self.last_flycheck_error {
167            status.health |= lsp_ext::Health::Warning;
168            message.push_str(err);
169            message.push('\n');
170        }
171
172        if self.config.linked_or_discovered_projects().is_empty()
173            && self.config.detached_files().is_empty()
174        {
175            status.health |= lsp_ext::Health::Warning;
176            message.push_str("Failed to discover workspace.\n");
177            message.push_str("Consider adding the `Cargo.toml` of the workspace to the [`linkedProjects`](https://rust-analyzer.github.io/book/configuration.html#linkedProjects) setting.\n\n");
178        }
179        if self.fetch_workspace_error().is_err() {
180            status.health |= lsp_ext::Health::Error;
181            message.push_str("Failed to load workspaces.");
182
183            if self.config.has_linked_projects() {
184                message.push_str(
185                    "`rust-analyzer.linkedProjects` have been specified, which may be incorrect. Specified project paths:\n",
186                );
187                message
188                    .push_str(&format!("    {}", self.config.linked_manifests().format("\n    ")));
189                if self.config.has_linked_project_jsons() {
190                    message.push_str("\nAdditionally, one or more project jsons are specified")
191                }
192            }
193            message.push_str("\n\n");
194        }
195
196        if !self.workspaces.is_empty() {
197            self.check_workspaces_msrv().for_each(|e| {
198                status.health |= lsp_ext::Health::Warning;
199                format_to!(message, "{e}");
200            });
201
202            let proc_macro_clients = self.proc_macro_clients.iter().chain(iter::repeat(&None));
203
204            for (ws, proc_macro_client) in self.workspaces.iter().zip(proc_macro_clients) {
205                if let ProjectWorkspaceKind::Cargo { error: Some(error), .. }
206                | ProjectWorkspaceKind::DetachedFile {
207                    cargo: Some((_, _, Some(error))), ..
208                } = &ws.kind
209                {
210                    status.health |= lsp_ext::Health::Warning;
211                    format_to!(
212                        message,
213                        "Failed to read Cargo metadata with dependencies for `{}`: {:#}\n\n",
214                        ws.manifest_or_root(),
215                        error
216                    );
217                }
218                if let Some(err) = ws.sysroot.error() {
219                    status.health |= lsp_ext::Health::Warning;
220                    format_to!(
221                        message,
222                        "Workspace `{}` has sysroot errors: ",
223                        ws.manifest_or_root()
224                    );
225                    message.push_str(err);
226                    message.push_str("\n\n");
227                }
228                if let Some(err) = ws.sysroot.metadata_error() {
229                    status.health |= lsp_ext::Health::Warning;
230                    format_to!(
231                        message,
232                        "Failed to read Cargo metadata with dependencies for sysroot of `{}`: ",
233                        ws.manifest_or_root()
234                    );
235                    message.push_str(err);
236                    message.push_str("\n\n");
237                }
238                if let ProjectWorkspaceKind::Cargo { rustc: Err(Some(err)), .. } = &ws.kind {
239                    status.health |= lsp_ext::Health::Warning;
240                    format_to!(
241                        message,
242                        "Failed loading rustc_private crates for workspace `{}`: ",
243                        ws.manifest_or_root()
244                    );
245                    message.push_str(err);
246                    message.push_str("\n\n");
247                };
248                match proc_macro_client {
249                    Some(Err(err)) => {
250                        status.health |= lsp_ext::Health::Warning;
251                        format_to!(
252                            message,
253                            "Failed spawning proc-macro server for workspace `{}`: {err}",
254                            ws.manifest_or_root()
255                        );
256                        message.push_str("\n\n");
257                    }
258                    Some(Ok(client)) => {
259                        if let Some(err) = client.exited() {
260                            status.health |= lsp_ext::Health::Warning;
261                            format_to!(
262                                message,
263                                "proc-macro server for workspace `{}` exited: {err}",
264                                ws.manifest_or_root()
265                            );
266                            message.push_str("\n\n");
267                        }
268                    }
269                    // sysroot was explicitly not set so we didn't discover a server
270                    None => {}
271                }
272            }
273        }
274
275        if !message.is_empty() {
276            status.message = Some(message.trim_end().to_owned());
277        }
278
279        status
280    }
281
282    pub(crate) fn fetch_workspaces(
283        &mut self,
284        cause: Cause,
285        path: Option<AbsPathBuf>,
286        force_crate_graph_reload: bool,
287    ) {
288        info!(%cause, "will fetch workspaces");
289
290        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, {
291            let linked_projects = self.config.linked_or_discovered_projects();
292            let detached_files: Vec<_> = self
293                .config
294                .detached_files()
295                .iter()
296                .cloned()
297                .map(ManifestPath::try_from)
298                .filter_map(Result::ok)
299                .collect();
300            let cargo_config = self.config.cargo(None);
301            let discover_command = self.config.discover_workspace_config().cloned();
302            let is_quiescent = !(self.discover_jobs_active > 0
303                || self.vfs_progress_config_version < self.vfs_config_version
304                || !self.vfs_done);
305
306            move |sender| {
307                let progress = {
308                    let sender = sender.clone();
309                    move |msg| {
310                        sender
311                            .send(Task::FetchWorkspace(ProjectWorkspaceProgress::Report(msg)))
312                            .unwrap()
313                    }
314                };
315
316                sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();
317
318                if let (Some(_command), Some(path)) = (&discover_command, &path) {
319                    let build = linked_projects.iter().find_map(|project| match project {
320                        LinkedProject::InlineProjectJson(it) => it.crate_by_buildfile(path),
321                        _ => None,
322                    });
323
324                    if let Some(build) = build
325                        && is_quiescent
326                    {
327                        let path = AbsPathBuf::try_from(build.build_file)
328                            .expect("Unable to convert to an AbsPath");
329                        let arg = DiscoverProjectParam::Buildfile(path);
330                        sender.send(Task::DiscoverLinkedProjects(arg)).unwrap();
331                    }
332                }
333
334                let mut workspaces: Vec<_> = linked_projects
335                    .iter()
336                    .map(|project| match project {
337                        LinkedProject::ProjectManifest(manifest) => {
338                            debug!(path = %manifest, "loading project from manifest");
339
340                            project_model::ProjectWorkspace::load(
341                                manifest.clone(),
342                                &cargo_config,
343                                &progress,
344                            )
345                        }
346                        LinkedProject::InlineProjectJson(it) => {
347                            let workspace = project_model::ProjectWorkspace::load_inline(
348                                it.clone(),
349                                &cargo_config,
350                                &progress,
351                            );
352                            Ok(workspace)
353                        }
354                    })
355                    .collect();
356
357                let mut i = 0;
358                while i < workspaces.len() {
359                    if let Ok(w) = &workspaces[i] {
360                        let dupes: Vec<_> = workspaces[i + 1..]
361                            .iter()
362                            .positions(|it| it.as_ref().is_ok_and(|ws| ws.eq_ignore_build_data(w)))
363                            .collect();
364                        dupes.into_iter().rev().for_each(|d| {
365                            _ = workspaces.remove(d + i + 1);
366                        });
367                    }
368                    i += 1;
369                }
370
371                if !detached_files.is_empty() {
372                    workspaces.extend(project_model::ProjectWorkspace::load_detached_files(
373                        detached_files,
374                        &cargo_config,
375                    ));
376                }
377
378                info!(?workspaces, "did fetch workspaces");
379                sender
380                    .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(
381                        workspaces,
382                        force_crate_graph_reload,
383                    )))
384                    .unwrap();
385            }
386        });
387    }
388
389    pub(crate) fn fetch_build_data(&mut self, cause: Cause) {
390        info!(%cause, "will fetch build data");
391        let workspaces = Arc::clone(&self.workspaces);
392        let config = self.config.cargo(None);
393        let root_path = self.config.default_root_path().clone();
394
395        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
396            sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap();
397
398            let progress = {
399                let sender = sender.clone();
400                move |msg| {
401                    sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
402                }
403            };
404            let res = ProjectWorkspace::run_all_build_scripts(
405                &workspaces,
406                &config,
407                &progress,
408                &root_path,
409            );
410
411            sender.send(Task::FetchBuildData(BuildDataProgress::End((workspaces, res)))).unwrap();
412        });
413    }
414
415    pub(crate) fn fetch_proc_macros(
416        &mut self,
417        cause: Cause,
418        mut change: ChangeWithProcMacros,
419        paths: Vec<ProcMacroPaths>,
420    ) {
421        info!(%cause, "will load proc macros");
422        let ignored_proc_macros = self.config.ignored_proc_macros(None).clone();
423        let proc_macro_clients = self.proc_macro_clients.clone();
424
425        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
426            sender.send(Task::LoadProcMacros(ProcMacroProgress::Begin)).unwrap();
427
428            let ignored_proc_macros = &ignored_proc_macros;
429            let progress = {
430                let sender = sender.clone();
431                &move |msg| {
432                    sender.send(Task::LoadProcMacros(ProcMacroProgress::Report(msg))).unwrap()
433                }
434            };
435
436            let mut builder = ProcMacrosBuilder::default();
437            let proc_macro_clients = proc_macro_clients.iter().chain(iter::repeat(&None));
438            for (client, paths) in proc_macro_clients.zip(paths) {
439                for (crate_id, res) in paths.iter() {
440                    let expansion_res = match client {
441                        Some(Ok(client)) => match res {
442                            Ok((crate_name, path)) => {
443                                progress(path.to_string());
444                                let ignored_proc_macros = ignored_proc_macros
445                                    .iter()
446                                    .find_map(|(name, macros)| {
447                                        eq_ignore_underscore(name, crate_name).then_some(&**macros)
448                                    })
449                                    .unwrap_or_default();
450
451                                load_proc_macro(client, path, ignored_proc_macros)
452                            }
453                            Err(e) => Err(e.clone()),
454                        },
455                        Some(Err(e)) => Err(ProcMacroLoadingError::ProcMacroSrvError(
456                            e.to_string().into_boxed_str(),
457                        )),
458                        None => Err(ProcMacroLoadingError::ProcMacroSrvError(
459                            "proc-macro-srv is not running".into(),
460                        )),
461                    };
462                    builder.insert(*crate_id, expansion_res)
463                }
464            }
465
466            change.set_proc_macros(builder);
467            sender.send(Task::LoadProcMacros(ProcMacroProgress::End(change))).unwrap();
468        });
469    }
470
471    pub(crate) fn switch_workspaces(&mut self, cause: Cause) {
472        let _p = tracing::info_span!("GlobalState::switch_workspaces").entered();
473        tracing::info!(%cause, "will switch workspaces");
474
475        let Some(FetchWorkspaceResponse { workspaces, force_crate_graph_reload }) =
476            self.fetch_workspaces_queue.last_op_result()
477        else {
478            return;
479        };
480        let switching_from_empty_workspace = self.workspaces.is_empty();
481
482        info!(%cause, ?force_crate_graph_reload, %switching_from_empty_workspace);
483        if self.fetch_workspace_error().is_err() && !switching_from_empty_workspace {
484            if *force_crate_graph_reload {
485                self.recreate_crate_graph(cause, false);
486            }
487            // It only makes sense to switch to a partially broken workspace
488            // if we don't have any workspace at all yet.
489            return;
490        }
491
492        let workspaces =
493            workspaces.iter().filter_map(|res| res.as_ref().ok().cloned()).collect::<Vec<_>>();
494
495        let same_workspaces = workspaces.len() == self.workspaces.len()
496            && workspaces
497                .iter()
498                .zip(self.workspaces.iter())
499                .all(|(l, r)| l.eq_ignore_build_data(r));
500
501        if same_workspaces {
502            if switching_from_empty_workspace {
503                // Switching from empty to empty is a no-op
504                return;
505            }
506            if let Some(FetchBuildDataResponse { workspaces, build_scripts }) =
507                self.fetch_build_data_queue.last_op_result()
508            {
509                if Arc::ptr_eq(workspaces, &self.workspaces) {
510                    info!("set build scripts to workspaces");
511
512                    let workspaces = workspaces
513                        .iter()
514                        .cloned()
515                        .zip(build_scripts)
516                        .map(|(mut ws, bs)| {
517                            ws.set_build_scripts(bs.as_ref().ok().cloned().unwrap_or_default());
518                            ws
519                        })
520                        .collect::<Vec<_>>();
521                    // Workspaces are the same, but we've updated build data.
522                    info!("same workspace, but new build data");
523                    self.workspaces = Arc::new(workspaces);
524                } else {
525                    info!("build scripts do not match the version of the active workspace");
526                    if *force_crate_graph_reload {
527                        self.recreate_crate_graph(cause, switching_from_empty_workspace);
528                    }
529
530                    // Current build scripts do not match the version of the active
531                    // workspace, so there's nothing for us to update.
532                    return;
533                }
534            } else {
535                if *force_crate_graph_reload {
536                    self.recreate_crate_graph(cause, switching_from_empty_workspace);
537                }
538
539                // No build scripts but unchanged workspaces, nothing to do here
540                return;
541            }
542        } else {
543            info!("abandon build scripts for workspaces");
544
545            // Here, we completely changed the workspace (Cargo.toml edit), so
546            // we don't care about build-script results, they are stale.
547            // FIXME: can we abort the build scripts here if they are already running?
548            self.workspaces = Arc::new(workspaces);
549            self.check_workspaces_msrv().for_each(|message| {
550                self.send_notification::<lsp_types::notification::ShowMessage>(
551                    lsp_types::ShowMessageParams { typ: lsp_types::MessageType::WARNING, message },
552                );
553            });
554
555            if self.config.run_build_scripts(None) {
556                self.build_deps_changed = false;
557                self.fetch_build_data_queue.request_op("workspace updated".to_owned(), ());
558
559                if !switching_from_empty_workspace {
560                    // `switch_workspaces()` will be called again when build scripts already run, which should
561                    // take a short time. If we update the workspace now we will invalidate proc macros and cfgs,
562                    // and then when build scripts complete we will invalidate them again.
563                    return;
564                }
565            }
566        }
567
568        if let FilesWatcher::Client = self.config.files().watcher {
569            let filter = self
570                .workspaces
571                .iter()
572                .flat_map(|ws| ws.to_roots())
573                .filter(|it| it.is_local)
574                .map(|it| it.include);
575
576            let mut watchers: Vec<FileSystemWatcher> =
577                if self.config.did_change_watched_files_relative_pattern_support() {
578                    // When relative patterns are supported by the client, prefer using them
579                    filter
580                        .flat_map(|include| {
581                            include.into_iter().flat_map(|base| {
582                                [
583                                    (base.clone(), "**/*.rs"),
584                                    (base.clone(), "**/Cargo.{lock,toml}"),
585                                    (base.clone(), "**/rust-analyzer.toml"),
586                                    (base, "**/*.md"),
587                                ]
588                            })
589                        })
590                        .map(|(base, pat)| lsp_types::FileSystemWatcher {
591                            glob_pattern: lsp_types::GlobPattern::Relative(
592                                lsp_types::RelativePattern {
593                                    base_uri: lsp_types::OneOf::Right(
594                                        lsp_types::Url::from_file_path(base).unwrap(),
595                                    ),
596                                    pattern: pat.to_owned(),
597                                },
598                            ),
599                            kind: None,
600                        })
601                        .collect()
602                } else {
603                    // When they're not, integrate the base to make them into absolute patterns
604                    filter
605                        .flat_map(|include| {
606                            include.into_iter().flat_map(|base| {
607                                [
608                                    format!("{base}/**/*.rs"),
609                                    format!("{base}/**/Cargo.{{toml,lock}}"),
610                                    format!("{base}/**/rust-analyzer.toml"),
611                                    format!("{base}/**/*.md"),
612                                ]
613                            })
614                        })
615                        .map(|glob_pattern| lsp_types::FileSystemWatcher {
616                            glob_pattern: lsp_types::GlobPattern::String(glob_pattern),
617                            kind: None,
618                        })
619                        .collect()
620                };
621
622            // Also explicitly watch any build files configured in JSON project files.
623            for ws in self.workspaces.iter() {
624                if let ProjectWorkspaceKind::Json(project_json) = &ws.kind {
625                    for (_, krate) in project_json.crates() {
626                        let Some(build) = &krate.build else {
627                            continue;
628                        };
629                        watchers.push(lsp_types::FileSystemWatcher {
630                            glob_pattern: lsp_types::GlobPattern::String(
631                                build.build_file.to_string(),
632                            ),
633                            kind: None,
634                        });
635                    }
636                }
637            }
638
639            watchers.extend(
640                iter::once(Config::user_config_dir_path().as_deref())
641                    .chain(self.workspaces.iter().map(|ws| ws.manifest().map(ManifestPath::as_ref)))
642                    .flatten()
643                    .map(|glob_pattern| lsp_types::FileSystemWatcher {
644                        glob_pattern: lsp_types::GlobPattern::String(glob_pattern.to_string()),
645                        kind: None,
646                    }),
647            );
648
649            let registration_options =
650                lsp_types::DidChangeWatchedFilesRegistrationOptions { watchers };
651            let registration = lsp_types::Registration {
652                id: "workspace/didChangeWatchedFiles".to_owned(),
653                method: "workspace/didChangeWatchedFiles".to_owned(),
654                register_options: Some(serde_json::to_value(registration_options).unwrap()),
655            };
656            self.send_request::<lsp_types::request::RegisterCapability>(
657                lsp_types::RegistrationParams { registrations: vec![registration] },
658                |_, _| (),
659            );
660        }
661
662        let files_config = self.config.files();
663        let project_folders = ProjectFolders::new(
664            &self.workspaces,
665            &files_config.exclude,
666            Config::user_config_dir_path().as_deref(),
667        );
668
669        if (self.proc_macro_clients.len() < self.workspaces.len() || !same_workspaces)
670            && self.config.expand_proc_macros()
671        {
672            info!("Spawning proc-macro servers");
673
674            self.proc_macro_clients = Arc::from_iter(self.workspaces.iter().map(|ws| {
675                let path = match self.config.proc_macro_srv() {
676                    Some(path) => path,
677                    None => match ws.find_sysroot_proc_macro_srv()? {
678                        Ok(path) => path,
679                        Err(e) => return Some(Err(e)),
680                    },
681                };
682
683                let env: FxHashMap<_, _> = match &ws.kind {
684                    ProjectWorkspaceKind::Cargo { cargo, .. }
685                    | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, ..)), .. } => cargo
686                        .env()
687                        .into_iter()
688                        .map(|(k, v)| (k.clone(), Some(v.clone())))
689                        .chain(
690                            self.config.extra_env(None).iter().map(|(k, v)| (k.clone(), v.clone())),
691                        )
692                        .chain(
693                            ws.sysroot
694                                .root()
695                                .filter(|_| {
696                                    !self.config.extra_env(None).contains_key("RUSTUP_TOOLCHAIN")
697                                        && std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
698                                })
699                                .map(|it| ("RUSTUP_TOOLCHAIN".to_owned(), Some(it.to_string()))),
700                        )
701                        .collect(),
702
703                    _ => Default::default(),
704                };
705                info!("Using proc-macro server at {path}");
706                let num_process = self.config.proc_macro_num_processes();
707
708                Some(
709                    ProcMacroClient::spawn(&path, &env, ws.toolchain.as_ref(), num_process)
710                        .map_err(|err| {
711                            tracing::error!(
712                                "Failed to run proc-macro server from path {path}, error: {err:?}",
713                            );
714                            anyhow::format_err!(
715                                "Failed to run proc-macro server from path {path}, error: {err:?}",
716                            )
717                        }),
718                )
719            }))
720        }
721
722        let watch = match files_config.watcher {
723            FilesWatcher::Client => vec![],
724            FilesWatcher::Server => project_folders.watch,
725        };
726        self.vfs_config_version += 1;
727        self.loader.handle.set_config(vfs::loader::Config {
728            load: project_folders.load,
729            watch,
730            version: self.vfs_config_version,
731        });
732        self.source_root_config = project_folders.source_root_config;
733        self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map());
734
735        info!(?cause, "recreating the crate graph");
736        self.recreate_crate_graph(cause, switching_from_empty_workspace);
737
738        info!("did switch workspaces");
739    }
740
741    fn recreate_crate_graph(&mut self, cause: String, initial_build: bool) {
742        info!(?cause, "Building Crate Graph");
743        self.report_progress(
744            "Building CrateGraph",
745            crate::lsp::utils::Progress::Begin,
746            None,
747            None,
748            None,
749        );
750
751        // crate graph construction relies on these paths, record them so when one of them gets
752        // deleted or created we trigger a reconstruction of the crate graph
753        self.crate_graph_file_dependencies.clear();
754        self.detached_files = self
755            .workspaces
756            .iter()
757            .filter_map(|ws| match &ws.kind {
758                ProjectWorkspaceKind::DetachedFile { file, .. } => Some(file.clone()),
759                _ => None,
760            })
761            .collect();
762
763        self.incomplete_crate_graph = false;
764        let (crate_graph, proc_macro_paths) = {
765            // Create crate graph from all the workspaces
766            let vfs = &self.vfs.read().0;
767            let load = |path: &AbsPath| {
768                let vfs_path = vfs::VfsPath::from(path.to_path_buf());
769                self.crate_graph_file_dependencies.insert(vfs_path.clone());
770                let file_id = vfs.file_id(&vfs_path);
771                self.incomplete_crate_graph |= file_id.is_none();
772                file_id.and_then(|(file_id, excluded)| {
773                    (excluded == vfs::FileExcluded::No).then_some(file_id)
774                })
775            };
776
777            ws_to_crate_graph(&self.workspaces, self.config.extra_env(None), load)
778        };
779        let mut change = ChangeWithProcMacros::default();
780        if initial_build || !self.config.expand_proc_macros() {
781            if self.config.expand_proc_macros() {
782                change.set_proc_macros(
783                    crate_graph
784                        .iter()
785                        .map(|id| (id, Err(ProcMacroLoadingError::NotYetBuilt)))
786                        .collect(),
787                );
788            } else {
789                change.set_proc_macros(
790                    crate_graph
791                        .iter()
792                        .map(|id| (id, Err(ProcMacroLoadingError::Disabled)))
793                        .collect(),
794                );
795            }
796
797            change.set_crate_graph(crate_graph);
798            self.analysis_host.apply_change(change);
799
800            self.finish_loading_crate_graph();
801        } else {
802            change.set_crate_graph(crate_graph);
803            self.fetch_proc_macros_queue.request_op(cause, (change, proc_macro_paths));
804        }
805
806        self.report_progress(
807            "Building CrateGraph",
808            crate::lsp::utils::Progress::End,
809            None,
810            None,
811            None,
812        );
813    }
814
815    pub(crate) fn finish_loading_crate_graph(&mut self) {
816        self.process_changes();
817        self.reload_flycheck();
818    }
819
820    pub(super) fn fetch_workspace_error(&self) -> Result<(), String> {
821        let mut buf = String::new();
822
823        let Some(FetchWorkspaceResponse { workspaces, .. }) =
824            self.fetch_workspaces_queue.last_op_result()
825        else {
826            return Ok(());
827        };
828
829        if workspaces.is_empty() && self.config.discover_workspace_config().is_none() {
830            stdx::format_to!(buf, "rust-analyzer failed to fetch workspace");
831        } else {
832            for ws in workspaces {
833                if let Err(err) = ws {
834                    stdx::format_to!(buf, "rust-analyzer failed to load workspace: {:#}\n", err);
835                }
836            }
837        }
838
839        if buf.is_empty() {
840            return Ok(());
841        }
842
843        Err(buf)
844    }
845
846    pub(super) fn fetch_build_data_error(&self) -> Result<(), String> {
847        let mut buf = String::new();
848
849        let Some(FetchBuildDataResponse { build_scripts, .. }) =
850            &self.fetch_build_data_queue.last_op_result()
851        else {
852            return Ok(());
853        };
854
855        for script in build_scripts {
856            match script {
857                Ok(data) => {
858                    if let Some(stderr) = data.error() {
859                        stdx::format_to!(buf, "{:#}\n", stderr)
860                    }
861                }
862                // io errors
863                Err(err) => stdx::format_to!(buf, "{:#}\n", err),
864            }
865        }
866
867        if buf.is_empty() { Ok(()) } else { Err(buf) }
868    }
869
870    fn reload_flycheck(&mut self) {
871        let _p = tracing::info_span!("GlobalState::reload_flycheck").entered();
872        let config = self.config.flycheck(None);
873        let sender = &self.flycheck_sender;
874        let invocation_strategy = config.invocation_strategy();
875        let next_gen =
876            self.flycheck.iter().map(FlycheckHandle::generation).max().unwrap_or_default() + 1;
877        let generation = Arc::new(AtomicUsize::new(next_gen));
878
879        self.flycheck = match invocation_strategy {
880            crate::flycheck::InvocationStrategy::Once => {
881                vec![FlycheckHandle::spawn(
882                    0,
883                    generation.clone(),
884                    sender.clone(),
885                    config,
886                    crate::flycheck::FlycheckConfigJson::default(),
887                    None,
888                    self.config.default_root_path().clone(),
889                    None,
890                    None,
891                )]
892            }
893            crate::flycheck::InvocationStrategy::PerWorkspace => {
894                self.workspaces
895                    .iter()
896                    .enumerate()
897                    .filter_map(|(id, ws)| {
898                        Some((
899                            id,
900                            match &ws.kind {
901                                ProjectWorkspaceKind::Cargo { cargo, .. }
902                                | ProjectWorkspaceKind::DetachedFile {
903                                    cargo: Some((cargo, _, _)),
904                                    ..
905                                } => (
906                                    crate::flycheck::FlycheckConfigJson::default(),
907                                    cargo.workspace_root(),
908                                    Some(cargo.manifest_path()),
909                                    Some(cargo.target_directory()),
910                                ),
911                                ProjectWorkspaceKind::Json(project) => {
912                                    let config_json = crate::flycheck::FlycheckConfigJson {
913                                        single_template: project
914                                            .runnable_template(project_json::RunnableKind::Flycheck)
915                                            .cloned(),
916                                    };
917                                    // Enable flychecks for json projects if a custom flycheck command was supplied
918                                    // in the workspace configuration.
919                                    match config {
920                                        _ if config_json.any_configured() => {
921                                            (config_json, project.path(), None, None)
922                                        }
923                                        FlycheckConfig::CustomCommand { .. } => {
924                                            (config_json, project.path(), None, None)
925                                        }
926                                        _ => return None,
927                                    }
928                                }
929                                ProjectWorkspaceKind::DetachedFile { .. } => return None,
930                            },
931                            ws.sysroot.root().map(ToOwned::to_owned),
932                        ))
933                    })
934                    .map(|(id, (config_json, root, manifest_path, target_dir), sysroot_root)| {
935                        FlycheckHandle::spawn(
936                            id,
937                            generation.clone(),
938                            sender.clone(),
939                            config.clone(),
940                            config_json,
941                            sysroot_root,
942                            root.to_path_buf(),
943                            manifest_path.map(|it| it.to_path_buf()),
944                            target_dir.map(|it| AsRef::<Utf8Path>::as_ref(it).to_path_buf()),
945                        )
946                    })
947                    .collect()
948            }
949        }
950        .into();
951        self.flycheck_formatted_commands = vec![];
952    }
953}
954
955// FIXME: Move this into load-cargo?
956pub fn ws_to_crate_graph(
957    workspaces: &[ProjectWorkspace],
958    extra_env: &FxHashMap<String, Option<String>>,
959    mut load: impl FnMut(&AbsPath) -> Option<vfs::FileId>,
960) -> (CrateGraphBuilder, Vec<ProcMacroPaths>) {
961    let mut crate_graph = CrateGraphBuilder::default();
962    let mut proc_macro_paths = Vec::default();
963    for ws in workspaces {
964        let (other, mut crate_proc_macros) = ws.to_crate_graph(&mut load, extra_env);
965
966        crate_graph.extend(other, &mut crate_proc_macros);
967        proc_macro_paths.push(crate_proc_macros);
968    }
969
970    crate_graph.shrink_to_fit();
971    proc_macro_paths.shrink_to_fit();
972    (crate_graph, proc_macro_paths)
973}
974
975pub(crate) fn should_refresh_for_change(
976    path: &AbsPath,
977    change_kind: ChangeKind,
978    additional_paths: &[&str],
979) -> bool {
980    // Note: build scripts are retriggered on file save, no refresh is necessary
981    const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
982    const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];
983
984    let file_name = match path.file_name() {
985        Some(it) => it,
986        None => return false,
987    };
988
989    if let "Cargo.toml" | "Cargo.lock" = file_name {
990        return true;
991    }
992
993    if additional_paths.contains(&file_name) {
994        return true;
995    }
996
997    // .cargo/config{.toml}
998    if matches!(file_name, "config.toml" | "config")
999        && path.parent().map(|parent| parent.as_str().ends_with(".cargo")).unwrap_or(false)
1000    {
1001        return true;
1002    }
1003
1004    // Everything below only matters when files are created or deleted
1005    if change_kind == ChangeKind::Modify {
1006        return false;
1007    }
1008
1009    if path.extension().unwrap_or_default() != "rs" {
1010        return false;
1011    }
1012
1013    if IMPLICIT_TARGET_FILES.iter().any(|it| path.as_str().ends_with(it)) {
1014        return true;
1015    }
1016    let parent = match path.parent() {
1017        Some(it) => it,
1018        None => return false,
1019    };
1020    if IMPLICIT_TARGET_DIRS.iter().any(|it| parent.as_str().ends_with(it)) {
1021        return true;
1022    }
1023    if file_name == "main.rs" {
1024        let grand_parent = match parent.parent() {
1025            Some(it) => it,
1026            None => return false,
1027        };
1028        if IMPLICIT_TARGET_DIRS.iter().any(|it| grand_parent.as_str().ends_with(it)) {
1029            return true;
1030        }
1031    }
1032    false
1033}
1034
1035/// Similar to [`str::eq_ignore_ascii_case`] but instead of ignoring
1036/// case, we say that `-` and `_` are equal.
1037fn eq_ignore_underscore(s1: &str, s2: &str) -> bool {
1038    if s1.len() != s2.len() {
1039        return false;
1040    }
1041
1042    s1.as_bytes().iter().zip(s2.as_bytes()).all(|(c1, c2)| {
1043        let c1_underscore = c1 == &b'_' || c1 == &b'-';
1044        let c2_underscore = c2 == &b'_' || c2 == &b'-';
1045
1046        c1 == c2 || (c1_underscore && c2_underscore)
1047    })
1048}