//! Project loading & configuration updates.
//!
//! This is quite tricky. The main problem is time and changes -- there's no
//! fixed "project" rust-analyzer is working with, "current project" is itself
//! mutable state. For example, when the user edits `Cargo.toml` by adding a new
//! dependency, project model changes. What's more, switching project model is
//! not instantaneous -- it takes time to run `cargo metadata` and (for proc
//! macros) `cargo check`.
//!
//! The main guiding principle here is, as elsewhere in rust-analyzer,
//! robustness. We try not to assume that the project model exists or is
//! correct. Instead, we try to provide a best-effort service. Even if the
//! project is currently loading and we don't have a full project model, we
//! still want to respond to various  requests.
// FIXME: This is a mess that needs some untangling work
use std::{iter, mem, sync::atomic::AtomicUsize, time::Duration};

use hir::{ChangeWithProcMacros, ProcMacrosBuilder};
use ide_db::{
    FxHashMap,
    base_db::{CrateGraphBuilder, ProcMacroLoadingError, ProcMacroPaths},
};
use itertools::Itertools;
use load_cargo::{ProjectFolders, load_proc_macro};
use lsp_types::FileSystemWatcher;
use paths::Utf8Path;
use proc_macro_api::ProcMacroClient;
use project_model::{
    ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts, project_json,
};
use stdx::{format_to, thread::ThreadIntent};
use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, ChangeKind};

use crate::{
    config::{Config, FilesWatcher, LinkedProject},
    flycheck::{FlycheckConfig, FlycheckHandle},
    global_state::{
        FetchBuildDataResponse, FetchWorkspaceRequest, FetchWorkspaceResponse, GlobalState,
    },
    lsp_ext,
    main_loop::{DiscoverProjectParam, Task},
    op_queue::Cause,
};
use tracing::{debug, info};

#[derive(Debug)]
pub(crate) enum ProjectWorkspaceProgress {
    Begin,
    Report(String),
    End(Vec<anyhow::Result<ProjectWorkspace>>, bool),
}

#[derive(Debug)]
pub(crate) enum BuildDataProgress {
    Begin,
    Report(String),
    End((Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)),
}

#[derive(Debug)]
pub(crate) enum ProcMacroProgress {
    Begin,
    Report(String),
    End(ChangeWithProcMacros),
}

impl GlobalState {
    /// Is the server quiescent?
    ///
    /// This indicates that we've fully loaded the projects and
    /// are ready to do semantic work.
    pub(crate) fn is_quiescent(&self) -> bool {
        self.vfs_done
            && self.fetch_ws_receiver.is_none()
            && !self.fetch_workspaces_queue.op_in_progress()
            && !self.fetch_build_data_queue.op_in_progress()
            && !self.fetch_proc_macros_queue.op_in_progress()
            && self.discover_jobs_active == 0
            && self.vfs_progress_config_version >= self.vfs_config_version
    }

    /// Is the server ready to respond to analysis dependent LSP requests?
    ///
    /// Unlike `is_quiescent`, this returns false when we're indexing
    /// the project, because we're holding the salsa lock and cannot
    /// respond to LSP requests that depend on salsa data.
    fn is_fully_ready(&self) -> bool {
        self.is_quiescent() && !self.prime_caches_queue.op_in_progress()
    }

    pub(crate) fn update_configuration(&mut self, config: Config) {
        let _p = tracing::info_span!("GlobalState::update_configuration").entered();
        let old_config = mem::replace(&mut self.config, Arc::new(config));
        if self.config.lru_parse_query_capacity() != old_config.lru_parse_query_capacity() {
            self.analysis_host.update_lru_capacity(self.config.lru_parse_query_capacity());
        }
        if self.config.lru_query_capacities_config() != old_config.lru_query_capacities_config() {
            self.analysis_host.update_lru_capacities(
                &self.config.lru_query_capacities_config().cloned().unwrap_or_default(),
            );
        }

        if self.config.linked_or_discovered_projects() != old_config.linked_or_discovered_projects()
        {
            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
            self.fetch_workspaces_queue.request_op("discovered projects changed".to_owned(), req)
        } else if self.config.flycheck(None) != old_config.flycheck(None) {
            self.reload_flycheck();
        }

        hir::db::set_expand_proc_attr_macros(
            self.analysis_host.raw_database_mut(),
            self.config.expand_proc_attr_macros(),
        );

        if self.config.cargo(None) != old_config.cargo(None) {
            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
            self.fetch_workspaces_queue.request_op("cargo config changed".to_owned(), req)
        }

        if self.config.cfg_set_test(None) != old_config.cfg_set_test(None) {
            let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
            self.fetch_workspaces_queue.request_op("cfg_set_test config changed".to_owned(), req)
        }
    }

    pub(crate) fn current_status(&self) -> lsp_ext::ServerStatusParams {
        let mut status = lsp_ext::ServerStatusParams {
            health: lsp_ext::Health::Ok,
            quiescent: self.is_fully_ready(),
            message: None,
        };
        let mut message = String::new();

        if !self.config.cargo_autoreload_config(None)
            && self.is_quiescent()
            && self.fetch_workspaces_queue.op_requested()
            && self.config.discover_workspace_config().is_none()
        {
            status.health |= lsp_ext::Health::Warning;
            message.push_str("Auto-reloading is disabled and the workspace has changed, a manual workspace reload is required.\n\n");
        }

        if self.build_deps_changed {
            status.health |= lsp_ext::Health::Warning;
            message.push_str(
                "Proc-macros and/or build scripts have changed and need to be rebuilt.\n\n",
            );
        }
        if self.fetch_build_data_error().is_err() {
            status.health |= lsp_ext::Health::Warning;
            message.push_str("Failed to run build scripts of some packages.\n\n");
            message.push_str(
                "Please refer to the language server logs for more details on the errors.",
            );
        }
        if let Some(err) = &self.config_errors {
            status.health |= lsp_ext::Health::Warning;
            format_to!(message, "{err}\n");
        }
        if let Some(err) = &self.last_flycheck_error {
            status.health |= lsp_ext::Health::Warning;
            message.push_str(err);
            message.push('\n');
        }

        if self.config.linked_or_discovered_projects().is_empty()
            && self.config.detached_files().is_empty()
        {
            status.health |= lsp_ext::Health::Warning;
            message.push_str("Failed to discover workspace.\n");
            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");
        }
        if self.fetch_workspace_error().is_err() {
            status.health |= lsp_ext::Health::Error;
            message.push_str("Failed to load workspaces.");

            if self.config.has_linked_projects() {
                message.push_str(
                    "`rust-analyzer.linkedProjects` have been specified, which may be incorrect. Specified project paths:\n",
                );
                message
                    .push_str(&format!("    {}", self.config.linked_manifests().format("\n    ")));
                if self.config.has_linked_project_jsons() {
                    message.push_str("\nAdditionally, one or more project jsons are specified")
                }
            }
            message.push_str("\n\n");
        }

        if !self.workspaces.is_empty() {
            self.check_workspaces_msrv().for_each(|e| {
                status.health |= lsp_ext::Health::Warning;
                format_to!(message, "{e}");
            });

            let proc_macro_clients = self.proc_macro_clients.iter().chain(iter::repeat(&None));

            for (ws, proc_macro_client) in self.workspaces.iter().zip(proc_macro_clients) {
                if let ProjectWorkspaceKind::Cargo { error: Some(error), .. }
                | ProjectWorkspaceKind::DetachedFile {
                    cargo: Some((_, _, Some(error))), ..
                } = &ws.kind
                {
                    status.health |= lsp_ext::Health::Warning;
                    format_to!(
                        message,
                        "Failed to read Cargo metadata with dependencies for `{}`: {:#}\n\n",
                        ws.manifest_or_root(),
                        error
                    );
                }
                if let Some(err) = ws.sysroot.error() {
                    status.health |= lsp_ext::Health::Warning;
                    format_to!(
                        message,
                        "Workspace `{}` has sysroot errors: ",
                        ws.manifest_or_root()
                    );
                    message.push_str(err);
                    message.push_str("\n\n");
                }
                if let Some(err) = ws.sysroot.metadata_error() {
                    status.health |= lsp_ext::Health::Warning;
                    format_to!(
                        message,
                        "Failed to read Cargo metadata with dependencies for sysroot of `{}`: ",
                        ws.manifest_or_root()
                    );
                    message.push_str(err);
                    message.push_str("\n\n");
                }
                if let ProjectWorkspaceKind::Cargo { rustc: Err(Some(err)), .. } = &ws.kind {
                    status.health |= lsp_ext::Health::Warning;
                    format_to!(
                        message,
                        "Failed loading rustc_private crates for workspace `{}`: ",
                        ws.manifest_or_root()
                    );
                    message.push_str(err);
                    message.push_str("\n\n");
                };
                match proc_macro_client {
                    Some(Err(err)) => {
                        status.health |= lsp_ext::Health::Warning;
                        format_to!(
                            message,
                            "Failed spawning proc-macro server for workspace `{}`: {err}",
                            ws.manifest_or_root()
                        );
                        message.push_str("\n\n");
                    }
                    Some(Ok(client)) => {
                        if let Some(err) = client.exited() {
                            status.health |= lsp_ext::Health::Warning;
                            format_to!(
                                message,
                                "proc-macro server for workspace `{}` exited: {err}",
                                ws.manifest_or_root()
                            );
                            message.push_str("\n\n");
                        }
                    }
                    // sysroot was explicitly not set so we didn't discover a server
                    None => {}
                }
            }
        }

        if !message.is_empty() {
            status.message = Some(message.trim_end().to_owned());
        }

        status
    }

    pub(crate) fn fetch_workspaces(
        &mut self,
        cause: Cause,
        path: Option<AbsPathBuf>,
        force_crate_graph_reload: bool,
    ) {
        info!(%cause, "will fetch workspaces");

        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, {
            let linked_projects = self.config.linked_or_discovered_projects();
            let detached_files: Vec<_> = self
                .config
                .detached_files()
                .iter()
                .cloned()
                .map(ManifestPath::try_from)
                .filter_map(Result::ok)
                .collect();
            let cargo_config = self.config.cargo(None);
            let discover_command = self.config.discover_workspace_config().cloned();
            let is_quiescent = !(self.discover_jobs_active > 0
                || self.vfs_progress_config_version < self.vfs_config_version
                || !self.vfs_done);

            move |sender| {
                let progress = {
                    let sender = sender.clone();
                    move |msg| {
                        sender
                            .send(Task::FetchWorkspace(ProjectWorkspaceProgress::Report(msg)))
                            .unwrap()
                    }
                };

                sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();

                if let (Some(_command), Some(path)) = (&discover_command, &path) {
                    let build = linked_projects.iter().find_map(|project| match project {
                        LinkedProject::InlineProjectJson(it) => it.crate_by_buildfile(path),
                        _ => None,
                    });

                    if let Some(build) = build
                        && is_quiescent
                    {
                        let path = AbsPathBuf::try_from(build.build_file)
                            .expect("Unable to convert to an AbsPath");
                        let arg = DiscoverProjectParam::Buildfile(path);
                        sender.send(Task::DiscoverLinkedProjects(arg)).unwrap();
                    }
                }

                let mut workspaces: Vec<_> = linked_projects
                    .iter()
                    .map(|project| match project {
                        LinkedProject::ProjectManifest(manifest) => {
                            debug!(path = %manifest, "loading project from manifest");

                            project_model::ProjectWorkspace::load(
                                manifest.clone(),
                                &cargo_config,
                                &progress,
                            )
                        }
                        LinkedProject::InlineProjectJson(it) => {
                            let workspace = project_model::ProjectWorkspace::load_inline(
                                it.clone(),
                                &cargo_config,
                                &progress,
                            );
                            Ok(workspace)
                        }
                    })
                    .collect();

                let mut i = 0;
                while i < workspaces.len() {
                    if let Ok(w) = &workspaces[i] {
                        let dupes: Vec<_> = workspaces[i + 1..]
                            .iter()
                            .positions(|it| it.as_ref().is_ok_and(|ws| ws.eq_ignore_build_data(w)))
                            .collect();
                        dupes.into_iter().rev().for_each(|d| {
                            _ = workspaces.remove(d + i + 1);
                        });
                    }
                    i += 1;
                }

                if !detached_files.is_empty() {
                    workspaces.extend(project_model::ProjectWorkspace::load_detached_files(
                        detached_files,
                        &cargo_config,
                    ));
                }

                info!(?workspaces, "did fetch workspaces");
                sender
                    .send(Task::FetchWorkspace(ProjectWorkspaceProgress::End(
                        workspaces,
                        force_crate_graph_reload,
                    )))
                    .unwrap();
            }
        });
    }

    pub(crate) fn fetch_build_data(&mut self, cause: Cause) {
        info!(%cause, "will fetch build data");
        let workspaces = Arc::clone(&self.workspaces);
        let config = self.config.cargo(None);
        let root_path = self.config.default_root_path().clone();

        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
            sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap();

            let progress = {
                let sender = sender.clone();
                move |msg| {
                    sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
                }
            };
            let res = ProjectWorkspace::run_all_build_scripts(
                &workspaces,
                &config,
                &progress,
                &root_path,
            );

            sender.send(Task::FetchBuildData(BuildDataProgress::End((workspaces, res)))).unwrap();
        });
    }

    pub(crate) fn fetch_proc_macros(
        &mut self,
        cause: Cause,
        mut change: ChangeWithProcMacros,
        paths: Vec<ProcMacroPaths>,
    ) {
        info!(%cause, "will load proc macros");
        let ignored_proc_macros = self.config.ignored_proc_macros(None).clone();
        let proc_macro_clients = self.proc_macro_clients.clone();

        self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
            sender.send(Task::LoadProcMacros(ProcMacroProgress::Begin)).unwrap();

            let ignored_proc_macros = &ignored_proc_macros;
            let progress = {
                let sender = sender.clone();
                &move |msg| {
                    sender.send(Task::LoadProcMacros(ProcMacroProgress::Report(msg))).unwrap()
                }
            };

            let mut builder = ProcMacrosBuilder::default();
            let proc_macro_clients = proc_macro_clients.iter().chain(iter::repeat(&None));
            for (client, paths) in proc_macro_clients.zip(paths) {
                for (crate_id, res) in paths.iter() {
                    let expansion_res = match client {
                        Some(Ok(client)) => match res {
                            Ok((crate_name, path)) => {
                                progress(path.to_string());
                                let ignored_proc_macros = ignored_proc_macros
                                    .iter()
                                    .find_map(|(name, macros)| {
                                        eq_ignore_underscore(name, crate_name).then_some(&**macros)
                                    })
                                    .unwrap_or_default();

                                load_proc_macro(client, path, ignored_proc_macros)
                            }
                            Err(e) => Err(e.clone()),
                        },
                        Some(Err(e)) => Err(ProcMacroLoadingError::ProcMacroSrvError(
                            e.to_string().into_boxed_str(),
                        )),
                        None => Err(ProcMacroLoadingError::ProcMacroSrvError(
                            "proc-macro-srv is not running".into(),
                        )),
                    };
                    builder.insert(*crate_id, expansion_res)
                }
            }

            change.set_proc_macros(builder);
            sender.send(Task::LoadProcMacros(ProcMacroProgress::End(change))).unwrap();
        });
    }

    pub(crate) fn switch_workspaces(&mut self, cause: Cause) -> Option<Duration> {
        let _p = tracing::info_span!("GlobalState::switch_workspaces").entered();
        tracing::info!(%cause, "will switch workspaces");

        let FetchWorkspaceResponse { workspaces, force_crate_graph_reload } =
            self.fetch_workspaces_queue.last_op_result()?;
        let switching_from_empty_workspace = self.workspaces.is_empty();

        info!(%cause, ?force_crate_graph_reload, %switching_from_empty_workspace);
        if self.fetch_workspace_error().is_err() && !switching_from_empty_workspace {
            if *force_crate_graph_reload {
                return self.recreate_crate_graph(cause, false);
            }
            // It only makes sense to switch to a partially broken workspace
            // if we don't have any workspace at all yet.
            return None;
        }

        let workspaces =
            workspaces.iter().filter_map(|res| res.as_ref().ok().cloned()).collect::<Vec<_>>();

        let same_workspaces = workspaces.len() == self.workspaces.len()
            && workspaces
                .iter()
                .zip(self.workspaces.iter())
                .all(|(l, r)| l.eq_ignore_build_data(r));

        if same_workspaces {
            if switching_from_empty_workspace {
                // Switching from empty to empty is a no-op
                return None;
            }
            if let Some(FetchBuildDataResponse { workspaces, build_scripts }) =
                self.fetch_build_data_queue.last_op_result()
            {
                if Arc::ptr_eq(workspaces, &self.workspaces) {
                    info!("set build scripts to workspaces");

                    let workspaces = workspaces
                        .iter()
                        .cloned()
                        .zip(build_scripts)
                        .map(|(mut ws, bs)| {
                            ws.set_build_scripts(bs.as_ref().ok().cloned().unwrap_or_default());
                            ws
                        })
                        .collect::<Vec<_>>();
                    // Workspaces are the same, but we've updated build data.
                    info!("same workspace, but new build data");
                    self.workspaces = Arc::new(workspaces);
                } else {
                    info!("build scripts do not match the version of the active workspace");
                    if *force_crate_graph_reload {
                        return self.recreate_crate_graph(cause, switching_from_empty_workspace);
                    }

                    // Current build scripts do not match the version of the active
                    // workspace, so there's nothing for us to update.
                    return None;
                }
            } else {
                if *force_crate_graph_reload {
                    return self.recreate_crate_graph(cause, switching_from_empty_workspace);
                }

                // No build scripts but unchanged workspaces, nothing to do here
                return None;
            }
        } else {
            info!("abandon build scripts for workspaces");

            // Here, we completely changed the workspace (Cargo.toml edit), so
            // we don't care about build-script results, they are stale.
            // FIXME: can we abort the build scripts here if they are already running?
            self.workspaces = Arc::new(workspaces);
            self.check_workspaces_msrv().for_each(|message| {
                self.send_notification::<lsp_types::ShowMessageNotification>(
                    lsp_types::ShowMessageParams { kind: lsp_types::MessageType::Warning, message },
                );
            });

            if self.config.run_build_scripts(None) {
                self.build_deps_changed = false;
                self.fetch_build_data_queue.request_op("workspace updated".to_owned(), ());

                if !switching_from_empty_workspace {
                    // `switch_workspaces()` will be called again when build scripts already run, which should
                    // take a short time. If we update the workspace now we will invalidate proc macros and cfgs,
                    // and then when build scripts complete we will invalidate them again.
                    return None;
                }
            }
        }

        if let FilesWatcher::Client = self.config.files().watcher {
            let filter = self
                .workspaces
                .iter()
                .flat_map(|ws| ws.to_roots())
                .filter(|it| it.is_local)
                .map(|it| it.include);

            let mut watchers: Vec<FileSystemWatcher> =
                if self.config.did_change_watched_files_relative_pattern_support() {
                    // When relative patterns are supported by the client, prefer using them
                    filter
                        .flat_map(|include| {
                            include.into_iter().flat_map(|base| {
                                [
                                    (base.clone(), "**/*.rs"),
                                    (base.clone(), "**/Cargo.{lock,toml}"),
                                    (base.clone(), "**/rust-analyzer.toml"),
                                    (base, "**/*.md"),
                                ]
                            })
                        })
                        .map(|(base, pat)| lsp_types::FileSystemWatcher {
                            glob_pattern: lsp_types::GlobPattern::RelativePattern(
                                lsp_types::RelativePattern {
                                    base_uri: lsp_types::BaseUri::Uri(
                                        lsp_types::Uri::from_file_path(base).unwrap(),
                                    ),
                                    pattern: pat.to_owned(),
                                },
                            ),
                            kind: None,
                        })
                        .collect()
                } else {
                    // When they're not, integrate the base to make them into absolute patterns
                    filter
                        .flat_map(|include| {
                            include.into_iter().flat_map(|base| {
                                [
                                    format!("{base}/**/*.rs"),
                                    format!("{base}/**/Cargo.{{toml,lock}}"),
                                    format!("{base}/**/rust-analyzer.toml"),
                                    format!("{base}/**/*.md"),
                                ]
                            })
                        })
                        .map(|glob_pattern| lsp_types::FileSystemWatcher {
                            glob_pattern: lsp_types::GlobPattern::Pattern(glob_pattern),
                            kind: None,
                        })
                        .collect()
                };

            // Also explicitly watch any build files configured in JSON project files.
            for ws in self.workspaces.iter() {
                if let ProjectWorkspaceKind::Json(project_json) = &ws.kind {
                    for (_, krate) in project_json.crates() {
                        let Some(build) = &krate.build else {
                            continue;
                        };
                        watchers.push(lsp_types::FileSystemWatcher {
                            glob_pattern: lsp_types::GlobPattern::Pattern(
                                build.build_file.to_string(),
                            ),
                            kind: None,
                        });
                    }
                }
            }

            watchers.extend(
                iter::once(Config::user_config_dir_path().as_deref())
                    .chain(self.workspaces.iter().map(|ws| ws.manifest().map(ManifestPath::as_ref)))
                    .flatten()
                    .map(|glob_pattern| lsp_types::FileSystemWatcher {
                        glob_pattern: lsp_types::GlobPattern::Pattern(glob_pattern.to_string()),
                        kind: None,
                    }),
            );

            let registration_options =
                lsp_types::DidChangeWatchedFilesRegistrationOptions { watchers };
            let registration = lsp_types::Registration {
                id: "workspace/didChangeWatchedFiles".to_owned(),
                method: "workspace/didChangeWatchedFiles".to_owned(),
                register_options: Some(serde_json::to_value(registration_options).unwrap()),
            };
            self.send_request::<lsp_types::RegistrationRequest>(
                lsp_types::RegistrationParams { registrations: vec![registration] },
                |_, _| (),
            );
        }

        let files_config = self.config.files();
        let project_folders = ProjectFolders::new(
            &self.workspaces,
            &files_config.exclude,
            Config::user_config_dir_path().as_deref(),
        );

        if (self.proc_macro_clients.len() < self.workspaces.len() || !same_workspaces)
            && self.config.expand_proc_macros()
        {
            info!("Spawning proc-macro servers");

            // Workspaces referring to the same proc-macro server executable (i.e. the same
            // sysroot) with an identical spawn environment share a single client, and thereby
            // a single set of server processes.
            let mut clients: Vec<(
                (AbsPathBuf, Option<semver::Version>, FxHashMap<String, Option<String>>),
                ProcMacroClient,
            )> = Vec::new();
            self.proc_macro_clients = Arc::from_iter(self.workspaces.iter().map(|ws| {
                let path = match self.config.proc_macro_srv() {
                    Some(path) => path,
                    None => match ws.find_sysroot_proc_macro_srv()? {
                        Ok(path) => path,
                        Err(e) => return Some(Err(e)),
                    },
                };

                let env: FxHashMap<_, _> = match &ws.kind {
                    ProjectWorkspaceKind::Cargo { cargo, .. }
                    | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, ..)), .. } => cargo
                        .env()
                        .into_iter()
                        .map(|(k, v)| (k.clone(), Some(v.clone())))
                        .chain(
                            self.config.extra_env(None).iter().map(|(k, v)| (k.clone(), v.clone())),
                        )
                        .chain(
                            ws.sysroot
                                .root()
                                .filter(|_| {
                                    !self.config.extra_env(None).contains_key("RUSTUP_TOOLCHAIN")
                                        && std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
                                })
                                .map(|it| ("RUSTUP_TOOLCHAIN".to_owned(), Some(it.to_string()))),
                        )
                        .collect(),

                    _ => Default::default(),
                };

                let key = (path, ws.toolchain.clone(), env);
                if let Some((_, client)) = clients.iter().find(|(k, _)| *k == key) {
                    return Some(Ok(client.clone()));
                }

                let (path, toolchain, env) = &key;
                info!("Spawning proc-macro server at {path}");
                let num_process = self.config.proc_macro_num_processes();

                Some(match ProcMacroClient::spawn(path, env, toolchain.as_ref(), num_process) {
                    Ok(client) => {
                        clients.push((key.clone(), client.clone()));
                        Ok(client)
                    }
                    Err(err) => {
                        tracing::error!(
                            "Failed to run proc-macro server from path {path}, error: {err:?}",
                        );
                        Err(anyhow::format_err!(
                            "Failed to run proc-macro server from path {path}, error: {err:?}",
                        ))
                    }
                })
            }))
        }

        let watch = match files_config.watcher {
            FilesWatcher::Client => vec![],
            FilesWatcher::Server => project_folders.watch,
        };
        self.vfs_config_version += 1;
        self.loader.handle.set_config(vfs::loader::Config {
            load: project_folders.load,
            watch,
            version: self.vfs_config_version,
        });
        self.source_root_config = project_folders.source_root_config;
        self.local_roots_parent_map = Arc::new(self.source_root_config.source_root_parent_map());

        info!(?cause, "recreating the crate graph");
        let cancellation_time = self.recreate_crate_graph(cause, switching_from_empty_workspace);

        info!("did switch workspaces");
        cancellation_time
    }

    fn recreate_crate_graph(&mut self, cause: String, initial_build: bool) -> Option<Duration> {
        info!(?cause, "Building Crate Graph");
        let mut cancellation_time = None;
        self.report_progress(
            "Building CrateGraph",
            crate::lsp::utils::Progress::Begin,
            None,
            None,
            None,
        );

        // crate graph construction relies on these paths, record them so when one of them gets
        // deleted or created we trigger a reconstruction of the crate graph
        self.crate_graph_file_dependencies.clear();
        self.detached_files = self
            .workspaces
            .iter()
            .filter_map(|ws| match &ws.kind {
                ProjectWorkspaceKind::DetachedFile { file, .. } => Some(file.clone()),
                _ => None,
            })
            .collect();

        self.incomplete_crate_graph = false;
        let (crate_graph, proc_macro_paths) = {
            // Create crate graph from all the workspaces
            let vfs = &self.vfs.read().0;
            let load = |path: &AbsPath| {
                let vfs_path = vfs::VfsPath::from(path.to_path_buf());
                self.crate_graph_file_dependencies.insert(vfs_path.clone());
                let file_id = vfs.file_id(&vfs_path);
                self.incomplete_crate_graph |= file_id.is_none();
                file_id.and_then(|(file_id, excluded)| {
                    (excluded == vfs::FileExcluded::No).then_some(file_id)
                })
            };

            ws_to_crate_graph(&self.workspaces, self.config.extra_env(None), load)
        };
        let mut change = ChangeWithProcMacros::default();
        if initial_build || !self.config.expand_proc_macros() {
            if self.config.expand_proc_macros() {
                change.set_proc_macros(
                    crate_graph
                        .iter()
                        .map(|id| (id, Err(ProcMacroLoadingError::NotYetBuilt)))
                        .collect(),
                );
            } else {
                change.set_proc_macros(
                    crate_graph
                        .iter()
                        .map(|id| (id, Err(ProcMacroLoadingError::Disabled)))
                        .collect(),
                );
            }

            change.set_crate_graph(crate_graph);
            cancellation_time = Some(self.analysis_host.apply_change(change));
            _ = self.finish_loading_crate_graph();
        } else {
            change.set_crate_graph(crate_graph);
            self.fetch_proc_macros_queue.request_op(cause, (change, proc_macro_paths));
        }

        self.report_progress(
            "Building CrateGraph",
            crate::lsp::utils::Progress::End,
            None,
            None,
            None,
        );
        cancellation_time
    }

    pub(crate) fn finish_loading_crate_graph(&mut self) -> Option<Duration> {
        let (_, cancellation_time) = self.process_changes();
        self.reload_flycheck();
        cancellation_time
    }

    pub(super) fn fetch_workspace_error(&self) -> Result<(), String> {
        let mut buf = String::new();

        let Some(FetchWorkspaceResponse { workspaces, .. }) =
            self.fetch_workspaces_queue.last_op_result()
        else {
            return Ok(());
        };

        if workspaces.is_empty() && self.config.discover_workspace_config().is_none() {
            stdx::format_to!(buf, "rust-analyzer failed to fetch workspace");
        } else {
            for ws in workspaces {
                if let Err(err) = ws {
                    stdx::format_to!(buf, "rust-analyzer failed to load workspace: {:#}\n", err);
                }
            }
        }

        if buf.is_empty() {
            return Ok(());
        }

        Err(buf)
    }

    pub(super) fn fetch_build_data_error(&self) -> Result<(), String> {
        let mut buf = String::new();

        let Some(FetchBuildDataResponse { build_scripts, .. }) =
            &self.fetch_build_data_queue.last_op_result()
        else {
            return Ok(());
        };

        for script in build_scripts {
            match script {
                Ok(data) => {
                    if let Some(stderr) = data.error() {
                        stdx::format_to!(buf, "{:#}\n", stderr)
                    }
                }
                // io errors
                Err(err) => stdx::format_to!(buf, "{:#}\n", err),
            }
        }

        if buf.is_empty() { Ok(()) } else { Err(buf) }
    }

    fn reload_flycheck(&mut self) {
        let _p = tracing::info_span!("GlobalState::reload_flycheck").entered();
        let config = self.config.flycheck(None);
        let sender = &self.flycheck_sender;
        let invocation_strategy = config.invocation_strategy();
        let next_gen =
            self.flycheck.iter().map(FlycheckHandle::generation).max().unwrap_or_default() + 1;
        let generation = Arc::new(AtomicUsize::new(next_gen));

        self.flycheck = match invocation_strategy {
            crate::flycheck::InvocationStrategy::Once => {
                vec![FlycheckHandle::spawn(
                    0,
                    generation.clone(),
                    sender.clone(),
                    config,
                    crate::flycheck::FlycheckConfigJson::default(),
                    None,
                    self.config.default_root_path().clone(),
                    None,
                    None,
                    None,
                    None,
                )]
            }
            crate::flycheck::InvocationStrategy::PerWorkspace => {
                self.workspaces
                    .iter()
                    .enumerate()
                    .filter_map(|(id, ws)| {
                        Some((
                            id,
                            match &ws.kind {
                                ProjectWorkspaceKind::Cargo { cargo, .. }
                                | ProjectWorkspaceKind::DetachedFile {
                                    cargo: Some((cargo, _, _)),
                                    ..
                                } => (
                                    crate::flycheck::FlycheckConfigJson::default(),
                                    cargo.workspace_root(),
                                    Some(cargo.manifest_path()),
                                    Some(cargo.target_directory()),
                                    cargo.build_directory(),
                                ),
                                ProjectWorkspaceKind::Json(project) => {
                                    let config_json = crate::flycheck::FlycheckConfigJson {
                                        single_template: project
                                            .runnable_template(project_json::RunnableKind::Flycheck)
                                            .cloned(),
                                    };
                                    // Enable flychecks for json projects if a custom flycheck command was supplied
                                    // in the workspace configuration.
                                    match config {
                                        _ if config_json.any_configured() => {
                                            (config_json, project.path(), None, None, None)
                                        }
                                        FlycheckConfig::CustomCommand { .. } => {
                                            (config_json, project.path(), None, None, None)
                                        }
                                        _ => return None,
                                    }
                                }
                                ProjectWorkspaceKind::DetachedFile { .. } => return None,
                            },
                            ws.sysroot.root().map(ToOwned::to_owned),
                            ws.toolchain.clone(),
                        ))
                    })
                    .map(
                        |(
                            id,
                            (config_json, root, manifest_path, target_dir, build_dir),
                            sysroot_root,
                            toolchain,
                        )| {
                            FlycheckHandle::spawn(
                                id,
                                generation.clone(),
                                sender.clone(),
                                config.clone(),
                                config_json,
                                sysroot_root,
                                root.to_path_buf(),
                                manifest_path.map(|it| it.to_path_buf()),
                                target_dir.map(|it| AsRef::<Utf8Path>::as_ref(it).to_path_buf()),
                                build_dir.map(|it| AsRef::<Utf8Path>::as_ref(it).to_path_buf()),
                                toolchain,
                            )
                        },
                    )
                    .collect()
            }
        }
        .into();
        self.flycheck_formatted_commands = vec![];
    }
}

// FIXME: Move this into load-cargo?
pub fn ws_to_crate_graph(
    workspaces: &[ProjectWorkspace],
    extra_env: &FxHashMap<String, Option<String>>,
    mut load: impl FnMut(&AbsPath) -> Option<vfs::FileId>,
) -> (CrateGraphBuilder, Vec<ProcMacroPaths>) {
    let mut crate_graph = CrateGraphBuilder::default();
    let mut proc_macro_paths = Vec::default();
    for ws in workspaces {
        let (other, mut crate_proc_macros) = ws.to_crate_graph(&mut load, extra_env);

        crate_graph.extend(other, &mut crate_proc_macros);
        proc_macro_paths.push(crate_proc_macros);
    }

    crate_graph.shrink_to_fit();
    proc_macro_paths.shrink_to_fit();
    (crate_graph, proc_macro_paths)
}

pub(crate) fn should_refresh_for_change(
    path: &AbsPath,
    change_kind: ChangeKind,
    additional_paths: &[&str],
) -> bool {
    // Note: build scripts are retriggered on file save, no refresh is necessary
    const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
    const IMPLICIT_TARGET_DIRS: &[&str] = &["src/bin", "examples", "tests", "benches"];

    let file_name = match path.file_name() {
        Some(it) => it,
        None => return false,
    };

    if let "Cargo.toml" | "Cargo.lock" = file_name {
        return true;
    }

    if additional_paths.contains(&file_name) {
        return true;
    }

    // .cargo/config{.toml}
    if matches!(file_name, "config.toml" | "config")
        && path.parent().map(|parent| parent.as_str().ends_with(".cargo")).unwrap_or(false)
    {
        return true;
    }

    // Everything below only matters when files are created or deleted
    if change_kind == ChangeKind::Modify {
        return false;
    }

    if path.extension().unwrap_or_default() != "rs" {
        return false;
    }

    if IMPLICIT_TARGET_FILES.iter().any(|it| path.as_str().ends_with(it)) {
        return true;
    }
    let parent = match path.parent() {
        Some(it) => it,
        None => return false,
    };
    if IMPLICIT_TARGET_DIRS.iter().any(|it| parent.as_str().ends_with(it)) {
        return true;
    }
    if file_name == "main.rs" {
        let grand_parent = match parent.parent() {
            Some(it) => it,
            None => return false,
        };
        if IMPLICIT_TARGET_DIRS.iter().any(|it| grand_parent.as_str().ends_with(it)) {
            return true;
        }
    }
    false
}

/// Similar to [`str::eq_ignore_ascii_case`] but instead of ignoring
/// case, we say that `-` and `_` are equal.
fn eq_ignore_underscore(s1: &str, s2: &str) -> bool {
    if s1.len() != s2.len() {
        return false;
    }

    s1.as_bytes().iter().zip(s2.as_bytes()).all(|(c1, c2)| {
        let c1_underscore = c1 == &b'_' || c1 == &b'-';
        let c2_underscore = c2 == &b'_' || c2 == &b'-';

        c1 == c2 || (c1_underscore && c2_underscore)
    })
}
