Auto merge of #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](https://github.com/helix-editor/helix/pull/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](https://github.com/helix-editor/helix/pull/5748#issuecomment-1421012523). 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.
This commit is contained in:
commit
3c89945e78
@ -10,7 +10,6 @@ mod rustc_wrapper;
|
|||||||
use std::{env, fs, path::Path, process};
|
use std::{env, fs, path::Path, process};
|
||||||
|
|
||||||
use lsp_server::Connection;
|
use lsp_server::Connection;
|
||||||
use project_model::ProjectManifest;
|
|
||||||
use rust_analyzer::{cli::flags, config::Config, from_json, Result};
|
use rust_analyzer::{cli::flags, config::Config, from_json, Result};
|
||||||
use vfs::AbsPathBuf;
|
use vfs::AbsPathBuf;
|
||||||
|
|
||||||
@ -168,7 +167,18 @@ fn run_server() -> Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = Config::new(root_path, initialize_params.capabilities);
|
let workspace_roots = initialize_params
|
||||||
|
.workspace_folders
|
||||||
|
.map(|workspaces| {
|
||||||
|
workspaces
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|it| it.uri.to_file_path().ok())
|
||||||
|
.filter_map(|it| AbsPathBuf::try_from(it).ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.filter(|workspaces| !workspaces.is_empty())
|
||||||
|
.unwrap_or_else(|| vec![root_path.clone()]);
|
||||||
|
let mut config = Config::new(root_path, initialize_params.capabilities, workspace_roots);
|
||||||
if let Some(json) = initialize_params.initialization_options {
|
if let Some(json) = initialize_params.initialization_options {
|
||||||
if let Err(e) = config.update(json) {
|
if let Err(e) = config.update(json) {
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
@ -202,25 +212,8 @@ fn run_server() -> Result<()> {
|
|||||||
tracing::info!("Client '{}' {}", client_info.name, client_info.version.unwrap_or_default());
|
tracing::info!("Client '{}' {}", client_info.name, client_info.version.unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.linked_projects().is_empty() && config.detached_files().is_empty() {
|
if !config.has_linked_projects() && config.detached_files().is_empty() {
|
||||||
let workspace_roots = initialize_params
|
config.rediscover_workspaces();
|
||||||
.workspace_folders
|
|
||||||
.map(|workspaces| {
|
|
||||||
workspaces
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|it| it.uri.to_file_path().ok())
|
|
||||||
.filter_map(|it| AbsPathBuf::try_from(it).ok())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.filter(|workspaces| !workspaces.is_empty())
|
|
||||||
.unwrap_or_else(|| vec![config.root_path().clone()]);
|
|
||||||
|
|
||||||
let discovered = ProjectManifest::discover_all(&workspace_roots);
|
|
||||||
tracing::info!("discovered projects: {:?}", discovered);
|
|
||||||
if discovered.is_empty() {
|
|
||||||
tracing::error!("failed to find any projects in {:?}", workspace_roots);
|
|
||||||
}
|
|
||||||
config.discovered_projects = Some(discovered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rust_analyzer::main_loop(config, connection)?;
|
rust_analyzer::main_loop(config, connection)?;
|
||||||
|
@ -10,7 +10,8 @@ use lsp_types::{
|
|||||||
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities,
|
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities,
|
||||||
SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||||
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
||||||
WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,
|
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
|
||||||
|
WorkspaceServerCapabilities,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@ -80,7 +81,10 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities {
|
|||||||
color_provider: None,
|
color_provider: None,
|
||||||
execute_command_provider: None,
|
execute_command_provider: None,
|
||||||
workspace: Some(WorkspaceServerCapabilities {
|
workspace: Some(WorkspaceServerCapabilities {
|
||||||
workspace_folders: None,
|
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||||
|
supported: Some(true),
|
||||||
|
change_notifications: Some(OneOf::Left(true)),
|
||||||
|
}),
|
||||||
file_operations: Some(WorkspaceFileOperationsServerCapabilities {
|
file_operations: Some(WorkspaceFileOperationsServerCapabilities {
|
||||||
did_create: None,
|
did_create: None,
|
||||||
will_create: None,
|
will_create: None,
|
||||||
|
@ -524,6 +524,7 @@ impl Default for ConfigData {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub discovered_projects: Option<Vec<ProjectManifest>>,
|
pub discovered_projects: Option<Vec<ProjectManifest>>,
|
||||||
|
pub workspace_roots: Vec<AbsPathBuf>,
|
||||||
caps: lsp_types::ClientCapabilities,
|
caps: lsp_types::ClientCapabilities,
|
||||||
root_path: AbsPathBuf,
|
root_path: AbsPathBuf,
|
||||||
data: ConfigData,
|
data: ConfigData,
|
||||||
@ -720,7 +721,11 @@ impl fmt::Display for ConfigUpdateError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Self {
|
pub fn new(
|
||||||
|
root_path: AbsPathBuf,
|
||||||
|
caps: ClientCapabilities,
|
||||||
|
workspace_roots: Vec<AbsPathBuf>,
|
||||||
|
) -> Self {
|
||||||
Config {
|
Config {
|
||||||
caps,
|
caps,
|
||||||
data: ConfigData::default(),
|
data: ConfigData::default(),
|
||||||
@ -728,9 +733,19 @@ impl Config {
|
|||||||
discovered_projects: None,
|
discovered_projects: None,
|
||||||
root_path,
|
root_path,
|
||||||
snippets: Default::default(),
|
snippets: Default::default(),
|
||||||
|
workspace_roots,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rediscover_workspaces(&mut self) {
|
||||||
|
let discovered = ProjectManifest::discover_all(&self.workspace_roots);
|
||||||
|
tracing::info!("discovered projects: {:?}", discovered);
|
||||||
|
if discovered.is_empty() {
|
||||||
|
tracing::error!("failed to find any projects in {:?}", &self.workspace_roots);
|
||||||
|
}
|
||||||
|
self.discovered_projects = Some(discovered);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigUpdateError> {
|
pub fn update(&mut self, mut json: serde_json::Value) -> Result<(), ConfigUpdateError> {
|
||||||
tracing::info!("updating config from JSON: {:#}", json);
|
tracing::info!("updating config from JSON: {:#}", json);
|
||||||
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
|
if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) {
|
||||||
@ -827,6 +842,9 @@ macro_rules! try_or_def {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
pub fn has_linked_projects(&self) -> bool {
|
||||||
|
!self.data.linkedProjects.is_empty()
|
||||||
|
}
|
||||||
pub fn linked_projects(&self) -> Vec<LinkedProject> {
|
pub fn linked_projects(&self) -> Vec<LinkedProject> {
|
||||||
match self.data.linkedProjects.as_slice() {
|
match self.data.linkedProjects.as_slice() {
|
||||||
[] => match self.discovered_projects.as_ref() {
|
[] => match self.discovered_projects.as_ref() {
|
||||||
|
@ -534,7 +534,7 @@ mod tests {
|
|||||||
let (sender, _) = crossbeam_channel::unbounded();
|
let (sender, _) = crossbeam_channel::unbounded();
|
||||||
let state = GlobalState::new(
|
let state = GlobalState::new(
|
||||||
sender,
|
sender,
|
||||||
Config::new(workspace_root.to_path_buf(), ClientCapabilities::default()),
|
Config::new(workspace_root.to_path_buf(), ClientCapabilities::default(), Vec::new()),
|
||||||
);
|
);
|
||||||
let snap = state.snapshot();
|
let snap = state.snapshot();
|
||||||
let mut actual = map_rust_diagnostic_to_lsp(&config, &diagnostic, workspace_root, &snap);
|
let mut actual = map_rust_diagnostic_to_lsp(&config, &diagnostic, workspace_root, &snap);
|
||||||
|
@ -14,7 +14,7 @@ use ide_db::base_db::{SourceDatabaseExt, VfsPath};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lsp_server::{Connection, Notification, Request};
|
use lsp_server::{Connection, Notification, Request};
|
||||||
use lsp_types::notification::Notification as _;
|
use lsp_types::notification::Notification as _;
|
||||||
use vfs::{ChangeKind, FileId};
|
use vfs::{AbsPathBuf, ChangeKind, FileId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
@ -933,6 +933,30 @@ impl GlobalState {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?
|
})?
|
||||||
|
.on::<lsp_types::notification::DidChangeWorkspaceFolders>(|this, params| {
|
||||||
|
let config = Arc::make_mut(&mut this.config);
|
||||||
|
|
||||||
|
for workspace in params.event.removed {
|
||||||
|
let Ok(path) = workspace.uri.to_file_path() else { continue };
|
||||||
|
let Ok(path) = AbsPathBuf::try_from(path) else { continue };
|
||||||
|
let Some(position) = config.workspace_roots.iter().position(|it| it == &path) else { continue };
|
||||||
|
config.workspace_roots.remove(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = params
|
||||||
|
.event
|
||||||
|
.added
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|it| it.uri.to_file_path().ok())
|
||||||
|
.filter_map(|it| AbsPathBuf::try_from(it).ok());
|
||||||
|
config.workspace_roots.extend(added);
|
||||||
|
if !config.has_linked_projects() && config.detached_files().is_empty() {
|
||||||
|
config.rediscover_workspaces();
|
||||||
|
this.fetch_workspaces_queue.request_op("client workspaces changed".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
.on::<lsp_types::notification::DidChangeWatchedFiles>(|this, params| {
|
.on::<lsp_types::notification::DidChangeWatchedFiles>(|this, params| {
|
||||||
for change in params.changes {
|
for change in params.changes {
|
||||||
if let Ok(path) = from_proto::abs_path(&change.uri) {
|
if let Ok(path) = from_proto::abs_path(&change.uri) {
|
||||||
|
@ -137,6 +137,7 @@ impl<'a> Project<'a> {
|
|||||||
})),
|
})),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
config.discovered_projects = Some(discovered_projects);
|
config.discovered_projects = Some(discovered_projects);
|
||||||
config.update(self.config).expect("invalid config");
|
config.update(self.config).expect("invalid config");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user