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