From f69fd6930057eaba52722fb1b7e78fd8d94c8407 Mon Sep 17 00:00:00 2001 From: Bogdan Mircea Date: Tue, 5 Nov 2024 18:52:17 +0200 Subject: [PATCH 1/4] rust-analyzer/reload: add project_root watchers over filesToWatch --- crates/rust-analyzer/src/reload.rs | 40 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index 0b24833358dd..c4b829acec60 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -530,6 +530,13 @@ impl GlobalState { .filter(|it| it.is_local) .map(|it| it.include); + let additional_filters = self + .config + .discover_workspace_config() + .into_iter() + .flat_map(|cfg| cfg.files_to_watch.iter()) + .flat_map(|pat| self.workspaces.iter().map(move |ws| (ws.workspace_root(), pat))); + let mut watchers: Vec = if self.config.did_change_watched_files_relative_pattern_support() { // When relative patterns are supported by the client, prefer using them @@ -537,19 +544,23 @@ impl GlobalState { .flat_map(|include| { include.into_iter().flat_map(|base| { [ - (base.clone(), "**/*.rs"), - (base.clone(), "**/Cargo.{lock,toml}"), - (base, "**/rust-analyzer.toml"), + (base.clone(), "**/*.rs".to_owned()), + (base.clone(), "**/Cargo.{lock,toml}".to_owned()), + (base.clone(), "**/rust-analyzer.toml".to_owned()), ] }) }) - .map(|(base, pat)| lsp_types::FileSystemWatcher { + .chain( + additional_filters + .map(|(base, pat)| (base.to_owned(), format!("**/{pat}"))), + ) + .map(|(base, pattern)| lsp_types::FileSystemWatcher { glob_pattern: lsp_types::GlobPattern::Relative( lsp_types::RelativePattern { base_uri: lsp_types::OneOf::Right( lsp_types::Url::from_file_path(base).unwrap(), ), - pattern: pat.to_owned(), + pattern, }, ), kind: None, @@ -567,6 +578,7 @@ impl GlobalState { ] }) }) + .chain(additional_filters.map(|(base, pat)| format!("{base}/**/{pat}"))) .map(|glob_pattern| lsp_types::FileSystemWatcher { glob_pattern: lsp_types::GlobPattern::String(glob_pattern), kind: None, @@ -574,23 +586,6 @@ impl GlobalState { .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::String( - build.build_file.to_string(), - ), - kind: None, - }); - } - } - } - watchers.extend( iter::once(Config::user_config_path()) .chain(self.workspaces.iter().map(|ws| ws.manifest().map(ManifestPath::as_ref))) @@ -608,6 +603,7 @@ impl GlobalState { method: "workspace/didChangeWatchedFiles".to_owned(), register_options: Some(serde_json::to_value(registration_options).unwrap()), }; + self.send_request::( lsp_types::RegistrationParams { registrations: vec![registration] }, |_, _| (), From 12caf7932f94b795c893b71c9488c26f713857c4 Mon Sep 17 00:00:00 2001 From: Bogdan Mircea Date: Tue, 5 Nov 2024 18:55:22 +0200 Subject: [PATCH 2/4] rust-analyzer/main_loop: add opt-in for workspace discovery at startup --- crates/rust-analyzer/src/config.rs | 11 ++++++++++- crates/rust-analyzer/src/discover.rs | 7 +++++-- crates/rust-analyzer/src/main_loop.rs | 25 +++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index f5b0fcecf390..026277d81840 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -319,6 +319,9 @@ config_data! { /// is used to determine which build system-specific files should be watched in order to /// reload rust-analyzer. /// + /// Optionally, `run_at_startup` can be set so that project discovery runs when the workspace + /// is opened. Note that at startup no `DiscoverArgument` (described below) gets passed. + /// /// Below is an example of a valid configuration: /// ```json /// "rust-analyzer.workspace.discoverConfig": { @@ -329,7 +332,8 @@ config_data! { /// "progressLabel": "rust-analyzer", /// "filesToWatch": [ /// "BUCK" - /// ] + /// ], + /// "runAtStartup": true /// } /// ``` /// @@ -400,6 +404,9 @@ config_data! { /// } /// ``` /// + /// This argument is not passed to the startup discovery which is enabled by the + /// `runAtStartup` setting. + /// /// `DiscoverArgument::Path` is used to find and generate a `rust-project.json`, /// and therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to /// to update an existing workspace. As a reference for implementors, @@ -1138,6 +1145,8 @@ pub struct DiscoverWorkspaceConfig { pub command: Vec, pub progress_label: String, pub files_to_watch: Vec, + #[serde(default)] + pub run_at_startup: bool, } pub struct CallInfoConfig { diff --git a/crates/rust-analyzer/src/discover.rs b/crates/rust-analyzer/src/discover.rs index 96b164228efe..50ebb37e2555 100644 --- a/crates/rust-analyzer/src/discover.rs +++ b/crates/rust-analyzer/src/discover.rs @@ -43,14 +43,17 @@ impl DiscoverCommand { } /// Spawn the command inside [Discover] and report progress, if any. - pub(crate) fn spawn(&self, discover_arg: DiscoverArgument) -> io::Result { + pub(crate) fn spawn( + &self, + discover_arg: Option, + ) -> io::Result { let command = &self.command[0]; let args = &self.command[1..]; let args: Vec = args .iter() .map(|arg| { - if arg == ARG_PLACEHOLDER { + if arg == ARG_PLACEHOLDER && discover_arg.is_some() { serde_json::to_string(&discover_arg).expect("Unable to serialize args") } else { arg.to_owned() diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 73fce42437f7..0420defe5c5f 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -176,7 +176,28 @@ impl GlobalState { self.register_did_save_capability(additional_patterns); } - if self.config.discover_workspace_config().is_none() { + let mut ran_startup_discovery = false; + + if let Some(cfg) = self.config.discover_workspace_config() { + if cfg.run_at_startup && !self.discover_workspace_queue.op_in_progress() { + // the clone is unfortunately necessary to avoid a borrowck error when + // `self.report_progress` is called later + let title = &cfg.progress_label.clone(); + let command = cfg.command.clone(); + let discover = DiscoverCommand::new(self.discover_sender.clone(), command); + + self.report_progress(title, Progress::Begin, None, None, None); + self.discover_workspace_queue.request_op("startup discovery".to_owned(), ()); + let _ = self.discover_workspace_queue.should_start_op(); + + let handle = discover.spawn(None).unwrap(); + self.discover_handle = Some(handle); + + ran_startup_discovery = true; + } + } + + if self.config.discover_workspace_config().is_none() || ran_startup_discovery { self.fetch_workspaces_queue.request_op( "startup".to_owned(), FetchWorkspaceRequest { path: None, force_crate_graph_reload: false }, @@ -740,7 +761,7 @@ impl GlobalState { DiscoverProjectParam::Path(it) => DiscoverArgument::Path(it), }; - let handle = discover.spawn(arg).unwrap(); + let handle = discover.spawn(Some(arg)).unwrap(); self.discover_handle = Some(handle); } } From 44a97eb6981bc875ba63fb355513ff59812dde63 Mon Sep 17 00:00:00 2001 From: Bogdan Mircea Date: Tue, 5 Nov 2024 19:23:23 +0200 Subject: [PATCH 3/4] run cargo test --- docs/user/generated_config.adoc | 9 ++++++++- editors/code/package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index 463718835b91..0d1970a978a8 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -1001,6 +1001,9 @@ Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command is used to determine which build system-specific files should be watched in order to reload rust-analyzer. +Optionally, `run_at_startup` can be set so that project discovery runs when the workspace +is opened. Note that at startup no `DiscoverArgument` (described below) gets passed. + Below is an example of a valid configuration: ```json "rust-analyzer.workspace.discoverConfig": { @@ -1011,7 +1014,8 @@ Below is an example of a valid configuration: "progressLabel": "rust-analyzer", "filesToWatch": [ "BUCK" - ] + ], + "runAtStartup": true } ``` @@ -1082,6 +1086,9 @@ Similarly, the JSON representation of `DiscoverArgument::Buildfile` is: } ``` +This argument is not passed to the startup discovery which is enabled by the +`runAtStartup` setting. + `DiscoverArgument::Path` is used to find and generate a `rust-project.json`, and therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to to update an existing workspace. As a reference for implementors, diff --git a/editors/code/package.json b/editors/code/package.json index ccc8c0e38423..ba50b3dc68c7 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -2559,7 +2559,7 @@ "title": "workspace", "properties": { "rust-analyzer.workspace.discoverConfig": { - "markdownDescription": "Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].\n\n[`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.\n`progress_label` is used for the title in progress indicators, whereas `files_to_watch`\nis used to determine which build system-specific files should be watched in order to\nreload rust-analyzer.\n\nBelow is an example of a valid configuration:\n```json\n\"rust-analyzer.workspace.discoverConfig\": {\n \"command\": [\n \"rust-project\",\n \"develop-json\"\n ],\n \"progressLabel\": \"rust-analyzer\",\n \"filesToWatch\": [\n \"BUCK\"\n ]\n}\n```\n\n## On `DiscoverWorkspaceConfig::command`\n\n**Warning**: This format is provisional and subject to change.\n\n[`DiscoverWorkspaceConfig::command`] *must* return a JSON object\ncorresponding to `DiscoverProjectData::Finished`:\n\n```norun\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\n#[serde(rename_all = \"snake_case\")]\nenum DiscoverProjectData {\n Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },\n Error { error: String, source: Option },\n Progress { message: String },\n}\n```\n\nAs JSON, `DiscoverProjectData::Finished` is:\n\n```json\n{\n // the internally-tagged representation of the enum.\n \"kind\": \"finished\",\n // the file used by a non-Cargo build system to define\n // a package or target.\n \"buildfile\": \"rust-analyzer/BUILD\",\n // the contents of a rust-project.json, elided for brevity\n \"project\": {\n \"sysroot\": \"foo\",\n \"crates\": []\n }\n}\n```\n\nIt is encouraged, but not required, to use the other variants on\n`DiscoverProjectData` to provide a more polished end-user experience.\n\n`DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`,\nwhich will be substituted with the JSON-serialized form of the following\nenum:\n\n```norun\n#[derive(PartialEq, Clone, Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum DiscoverArgument {\n Path(AbsPathBuf),\n Buildfile(AbsPathBuf),\n}\n```\n\nThe JSON representation of `DiscoverArgument::Path` is:\n\n```json\n{\n \"path\": \"src/main.rs\"\n}\n```\n\nSimilarly, the JSON representation of `DiscoverArgument::Buildfile` is:\n\n```\n{\n \"buildfile\": \"BUILD\"\n}\n```\n\n`DiscoverArgument::Path` is used to find and generate a `rust-project.json`,\nand therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to\nto update an existing workspace. As a reference for implementors,\nbuck2's `rust-project` will likely be useful:\nhttps://github.com/facebook/buck2/tree/main/integrations/rust-project.", + "markdownDescription": "Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].\n\n[`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.\n`progress_label` is used for the title in progress indicators, whereas `files_to_watch`\nis used to determine which build system-specific files should be watched in order to\nreload rust-analyzer.\n\nOptionally, `run_at_startup` can be set so that project discovery runs when the workspace\nis opened. Note that at startup no `DiscoverArgument` (described below) gets passed.\n\nBelow is an example of a valid configuration:\n```json\n\"rust-analyzer.workspace.discoverConfig\": {\n \"command\": [\n \"rust-project\",\n \"develop-json\"\n ],\n \"progressLabel\": \"rust-analyzer\",\n \"filesToWatch\": [\n \"BUCK\"\n ],\n \"runAtStartup\": true\n}\n```\n\n## On `DiscoverWorkspaceConfig::command`\n\n**Warning**: This format is provisional and subject to change.\n\n[`DiscoverWorkspaceConfig::command`] *must* return a JSON object\ncorresponding to `DiscoverProjectData::Finished`:\n\n```norun\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\n#[serde(rename_all = \"snake_case\")]\nenum DiscoverProjectData {\n Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },\n Error { error: String, source: Option },\n Progress { message: String },\n}\n```\n\nAs JSON, `DiscoverProjectData::Finished` is:\n\n```json\n{\n // the internally-tagged representation of the enum.\n \"kind\": \"finished\",\n // the file used by a non-Cargo build system to define\n // a package or target.\n \"buildfile\": \"rust-analyzer/BUILD\",\n // the contents of a rust-project.json, elided for brevity\n \"project\": {\n \"sysroot\": \"foo\",\n \"crates\": []\n }\n}\n```\n\nIt is encouraged, but not required, to use the other variants on\n`DiscoverProjectData` to provide a more polished end-user experience.\n\n`DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`,\nwhich will be substituted with the JSON-serialized form of the following\nenum:\n\n```norun\n#[derive(PartialEq, Clone, Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum DiscoverArgument {\n Path(AbsPathBuf),\n Buildfile(AbsPathBuf),\n}\n```\n\nThe JSON representation of `DiscoverArgument::Path` is:\n\n```json\n{\n \"path\": \"src/main.rs\"\n}\n```\n\nSimilarly, the JSON representation of `DiscoverArgument::Buildfile` is:\n\n```\n{\n \"buildfile\": \"BUILD\"\n}\n```\n\nThis argument is not passed to the startup discovery which is enabled by the\n`runAtStartup` setting.\n\n`DiscoverArgument::Path` is used to find and generate a `rust-project.json`,\nand therefore, a workspace, whereas `DiscoverArgument::buildfile` is used to\nto update an existing workspace. As a reference for implementors,\nbuck2's `rust-project` will likely be useful:\nhttps://github.com/facebook/buck2/tree/main/integrations/rust-project.", "default": null, "anyOf": [ { From 1ff4e8d6140b0fb8deb890a803609df3eec98c2d Mon Sep 17 00:00:00 2001 From: Bogdan Mircea Date: Wed, 6 Nov 2024 16:39:50 +0200 Subject: [PATCH 4/4] rust-analyzer/main_loop: simplify workspace discovery at startup --- crates/rust-analyzer/src/main_loop.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 0420defe5c5f..19b822ecae7f 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -176,8 +176,6 @@ impl GlobalState { self.register_did_save_capability(additional_patterns); } - let mut ran_startup_discovery = false; - if let Some(cfg) = self.config.discover_workspace_config() { if cfg.run_at_startup && !self.discover_workspace_queue.op_in_progress() { // the clone is unfortunately necessary to avoid a borrowck error when @@ -192,12 +190,10 @@ impl GlobalState { let handle = discover.spawn(None).unwrap(); self.discover_handle = Some(handle); - - ran_startup_discovery = true; } } - if self.config.discover_workspace_config().is_none() || ran_startup_discovery { + if self.config.discover_workspace_config().is_none() { self.fetch_workspaces_queue.request_op( "startup".to_owned(), FetchWorkspaceRequest { path: None, force_crate_graph_reload: false },