Skip to content

Commit 3c89945

Browse files
committed
Auto merge of rust-lang#14098 - pascalkuthe:did_change_workspace_folder, r=Veykril
Support DidChangeWorkspaceFolders notifications This PR enables the `WorkspaceFoldersServerCapabilities` capability for rust-analyzer and implemented support for the associated [`DidChangeWorkspaceFolders`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWorkspaceFolders) notification to allow clients to update the list of `workspaceFolders` sent during initialization. ## Motivation This allows clients which lazily autodiscover their workspace roots (like the [helix editor](https://github.com/helix-editor/helix) once [my PR](helix-editor/helix#5748) lands) avoid spawning multiple instances of RA. Right now such clients are forced to either: * greedily discover all LSP roots in the workspace (precludes the ability to respond to new workspace roots) * spawn multiple instance of rust-analyzer (one for each root) * restart rust-analyzer whenever a new workspace is added Some example use-cases are shown [here](helix-editor/helix#5748 (comment)). This PR will also improve support for VSCode (and Atom) multi workspaces. ## Implementation The implementation was fairly straightforward as `rust-analyzer` already supports dynamically reloading workspaces, for example on configuration changes. Furthermore, rust-analyzer also already supports auto-discovering internal workspace from the `workspaceFolders` key in the initialization request. Therefore, the necessary logic just needed to be moved to a central place and reused.
2 parents b7836e4 + c7010ed commit 3c89945

File tree

6 files changed

+66
-26
lines changed

6 files changed

+66
-26
lines changed

crates/rust-analyzer/src/bin/main.rs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ mod rustc_wrapper;
1010
use std::{env, fs, path::Path, process};
1111

1212
use lsp_server::Connection;
13-
use project_model::ProjectManifest;
1413
use rust_analyzer::{cli::flags, config::Config, from_json, Result};
1514
use vfs::AbsPathBuf;
1615

@@ -168,7 +167,18 @@ fn run_server() -> Result<()> {
168167
}
169168
};
170169

171-
let mut config = Config::new(root_path, initialize_params.capabilities);
170+
let workspace_roots = initialize_params
171+
.workspace_folders
172+
.map(|workspaces| {
173+
workspaces
174+
.into_iter()
175+
.filter_map(|it| it.uri.to_file_path().ok())
176+
.filter_map(|it| AbsPathBuf::try_from(it).ok())
177+
.collect::<Vec<_>>()
178+
})
179+
.filter(|workspaces| !workspaces.is_empty())
180+
.unwrap_or_else(|| vec![root_path.clone()]);
181+
let mut config = Config::new(root_path, initialize_params.capabilities, workspace_roots);
172182
if let Some(json) = initialize_params.initialization_options {
173183
if let Err(e) = config.update(json) {
174184
use lsp_types::{
@@ -202,25 +212,8 @@ fn run_server() -> Result<()> {
202212
tracing::info!("Client '{}' {}", client_info.name, client_info.version.unwrap_or_default());
203213
}
204214

205-
if config.linked_projects().is_empty() && config.detached_files().is_empty() {
206-
let workspace_roots = initialize_params
207-
.workspace_folders
208-
.map(|workspaces| {
209-
workspaces
210-
.into_iter()
211-
.filter_map(|it| it.uri.to_file_path().ok())
212-
.filter_map(|it| AbsPathBuf::try_from(it).ok())
213-
.collect::<Vec<_>>()
214-
})
215-
.filter(|workspaces| !workspaces.is_empty())
216-
.unwrap_or_else(|| vec![config.root_path().clone()]);
217-
218-
let discovered = ProjectManifest::discover_all(&workspace_roots);
219-
tracing::info!("discovered projects: {:?}", discovered);
220-
if discovered.is_empty() {
221-
tracing::error!("failed to find any projects in {:?}", workspace_roots);
222-
}
223-
config.discovered_projects = Some(discovered);
215+
if !config.has_linked_projects() && config.detached_files().is_empty() {
216+
config.rediscover_workspaces();
224217
}
225218

226219
rust_analyzer::main_loop(config, connection)?;

crates/rust-analyzer/src/caps.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use lsp_types::{
1010
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities,
1111
SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
1212
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
13-
WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,
13+
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
14+
WorkspaceServerCapabilities,
1415
};
1516
use serde_json::json;
1617

@@ -80,7 +81,10 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities {
8081
color_provider: None,
8182
execute_command_provider: None,
8283
workspace: Some(WorkspaceServerCapabilities {
83-
workspace_folders: None,
84+
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
85+
supported: Some(true),
86+
change_notifications: Some(OneOf::Left(true)),
87+
}),
8488
file_operations: Some(WorkspaceFileOperationsServerCapabilities {
8589
did_create: None,
8690
will_create: None,

crates/rust-analyzer/src/config.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ impl Default for ConfigData {
524524
#[derive(Debug, Clone)]
525525
pub struct Config {
526526
pub discovered_projects: Option<Vec<ProjectManifest>>,
527+
pub workspace_roots: Vec<AbsPathBuf>,
527528
caps: lsp_types::ClientCapabilities,
528529
root_path: AbsPathBuf,
529530
data: ConfigData,
@@ -720,17 +721,31 @@ impl fmt::Display for ConfigUpdateError {
720721
}
721722

722723
impl Config {
723-
pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
724+
pub fn new(
725+
root_path: AbsPathBuf,
726+
caps: ClientCapabilities,
727+
workspace_roots: Vec<AbsPathBuf>,
728+
) -> Self {
724729
Config {
725730
caps,
726731
data: ConfigData::default(),
727732
detached_files: Vec::new(),
728733
discovered_projects: None,
729734
root_path,
730735
snippets: Default::default(),
736+
workspace_roots,
731737
}
732738
}
733739

740+
pub fn rediscover_workspaces(&mut self) {
741+
let discovered = ProjectManifest::discover_all(&self.workspace_roots);
742+
tracing::info!("discovered projects: {:?}", discovered);
743+
if discovered.is_empty() {
744+
tracing::error!("failed to find any projects in {:?}", &self.workspace_roots);
745+
}
746+
self.discovered_projects = Some(discovered);
747+
}
748+
734749
pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigUpdateError> {
735750
tracing::info!("updating config from JSON: {:#}", json);
736751
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
@@ -827,6 +842,9 @@ macro_rules! try_or_def {
827842
}
828843

829844
impl Config {
845+
pub fn has_linked_projects(&self) -> bool {
846+
!self.data.linkedProjects.is_empty()
847+
}
830848
pub fn linked_projects(&self) -> Vec<LinkedProject> {
831849
match self.data.linkedProjects.as_slice() {
832850
[] => match self.discovered_projects.as_ref() {

crates/rust-analyzer/src/diagnostics/to_proto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ mod tests {
534534
let (sender, _) = crossbeam_channel::unbounded();
535535
let state = GlobalState::new(
536536
sender,
537-
Config::new(workspace_root.to_path_buf(), ClientCapabilities::default()),
537+
Config::new(workspace_root.to_path_buf(), ClientCapabilities::default(), Vec::new()),
538538
);
539539
let snap = state.snapshot();
540540
let mut actual = map_rust_diagnostic_to_lsp(&config, &diagnostic, workspace_root, &snap);

crates/rust-analyzer/src/main_loop.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use ide_db::base_db::{SourceDatabaseExt, VfsPath};
1414
use itertools::Itertools;
1515
use lsp_server::{Connection, Notification, Request};
1616
use lsp_types::notification::Notification as _;
17-
use vfs::{ChangeKind, FileId};
17+
use vfs::{AbsPathBuf, ChangeKind, FileId};
1818

1919
use crate::{
2020
config::Config,
@@ -933,6 +933,30 @@ impl GlobalState {
933933

934934
Ok(())
935935
})?
936+
.on::<lsp_types::notification::DidChangeWorkspaceFolders>(|this, params| {
937+
let config = Arc::make_mut(&mut this.config);
938+
939+
for workspace in params.event.removed {
940+
let Ok(path) = workspace.uri.to_file_path() else { continue };
941+
let Ok(path) = AbsPathBuf::try_from(path) else { continue };
942+
let Some(position) = config.workspace_roots.iter().position(|it| it == &path) else { continue };
943+
config.workspace_roots.remove(position);
944+
}
945+
946+
let added = params
947+
.event
948+
.added
949+
.into_iter()
950+
.filter_map(|it| it.uri.to_file_path().ok())
951+
.filter_map(|it| AbsPathBuf::try_from(it).ok());
952+
config.workspace_roots.extend(added);
953+
if !config.has_linked_projects() && config.detached_files().is_empty() {
954+
config.rediscover_workspaces();
955+
this.fetch_workspaces_queue.request_op("client workspaces changed".to_string())
956+
}
957+
958+
Ok(())
959+
})?
936960
.on::<lsp_types::notification::DidChangeWatchedFiles>(|this, params| {
937961
for change in params.changes {
938962
if let Ok(path) = from_proto::abs_path(&change.uri) {

crates/rust-analyzer/tests/slow-tests/support.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ impl<'a> Project<'a> {
137137
})),
138138
..Default::default()
139139
},
140+
Vec::new(),
140141
);
141142
config.discovered_projects = Some(discovered_projects);
142143
config.update(self.config).expect("invalid config");

0 commit comments

Comments
 (0)