LSP: Use PathBufs for workspace folders

Internally the LSP client should hold workspace folders as paths. Using
URLs for this type is inconvenient (since we compare it to paths) and
might cause mismatches because of URLs not being normalized. The URLs
must be paths anyways so we can convert these types lazily when we need
to send them to a server.
pull/11889/head
Michael Davis 3 months ago
parent a7651f5bf0
commit 7b034e719b
No known key found for this signature in database

@ -32,14 +32,17 @@ use tokio::{
}, },
}; };
fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { fn workspace_for_path(path: &Path) -> WorkspaceFolder {
let name = path
.iter()
.last()
.expect("workspace paths should be non-empty")
.to_string_lossy()
.to_string();
lsp::WorkspaceFolder { lsp::WorkspaceFolder {
name: uri name,
.path_segments() uri: lsp::Url::from_file_path(path).expect("absolute paths can be converted to `Url`s"),
.and_then(|segments| segments.last())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri,
} }
} }
@ -55,7 +58,7 @@ pub struct Client {
config: Option<Value>, config: Option<Value>,
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>, root_uri: Option<lsp::Url>,
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>, workspace_folders: Mutex<Vec<PathBuf>>,
initialize_notify: Arc<Notify>, initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing /// workspace folders added while the server is still initializing
req_timeout: u64, req_timeout: u64,
@ -80,16 +83,13 @@ impl Client {
&workspace, &workspace,
workspace_is_cwd, workspace_is_cwd,
); );
let root_uri = root
.as_ref()
.and_then(|root| lsp::Url::from_file_path(root).ok());
if self.root_path == root.unwrap_or(workspace) if &self.root_path == root.as_ref().unwrap_or(&workspace)
|| root_uri.as_ref().map_or(false, |root_uri| { || root.as_ref().map_or(false, |root| {
self.workspace_folders self.workspace_folders
.lock() .lock()
.iter() .iter()
.any(|workspace| &workspace.uri == root_uri) .any(|workspace| workspace == root)
}) })
{ {
// workspace URI is already registered so we can use this client // workspace URI is already registered so we can use this client
@ -113,15 +113,16 @@ impl Client {
// wait and see if anyone ever runs into it. // wait and see if anyone ever runs into it.
tokio::spawn(async move { tokio::spawn(async move {
client.initialize_notify.notified().await; client.initialize_notify.notified().await;
if let Some(workspace_folders_caps) = client if let Some((workspace_folders_caps, root)) = client
.capabilities() .capabilities()
.workspace .workspace
.as_ref() .as_ref()
.and_then(|cap| cap.workspace_folders.as_ref()) .and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false)) .filter(|cap| cap.supported.unwrap_or(false))
.zip(root)
{ {
client.add_workspace_folder( client.add_workspace_folder(
root_uri, root,
workspace_folders_caps.change_notifications.as_ref(), workspace_folders_caps.change_notifications.as_ref(),
); );
} }
@ -129,16 +130,14 @@ impl Client {
return true; return true;
}; };
if let Some(workspace_folders_caps) = capabilities if let Some((workspace_folders_caps, root)) = capabilities
.workspace .workspace
.as_ref() .as_ref()
.and_then(|cap| cap.workspace_folders.as_ref()) .and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false)) .filter(|cap| cap.supported.unwrap_or(false))
.zip(root)
{ {
self.add_workspace_folder( self.add_workspace_folder(root, workspace_folders_caps.change_notifications.as_ref());
root_uri,
workspace_folders_caps.change_notifications.as_ref(),
);
true true
} else { } else {
// the server doesn't support multi workspaces, we need a new client // the server doesn't support multi workspaces, we need a new client
@ -148,29 +147,19 @@ impl Client {
fn add_workspace_folder( fn add_workspace_folder(
&self, &self,
root_uri: Option<lsp::Url>, root: PathBuf,
change_notifications: Option<&OneOf<bool, String>>, change_notifications: Option<&OneOf<bool, String>>,
) { ) {
// root_uri is None just means that there isn't really any LSP workspace let workspace = workspace_for_path(&root);
// associated with this file. For servers that support multiple workspaces
// there is just one server so we can always just use that shared instance.
// No need to add a new workspace root here as there is no logical root for this file
// let the server deal with this
let Some(root_uri) = root_uri else {
return;
};
// server supports workspace folders, let's add the new root to the list // server supports workspace folders, let's add the new root to the list
self.workspace_folders self.workspace_folders.lock().push(root);
.lock()
.push(workspace_for_uri(root_uri.clone()));
if Some(&OneOf::Left(false)) == change_notifications { if Some(&OneOf::Left(false)) == change_notifications {
// server specifically opted out of DidWorkspaceChange notifications // server specifically opted out of DidWorkspaceChange notifications
// let's assume the server will request the workspace folders itself // let's assume the server will request the workspace folders itself
// and that we can therefore reuse the client (but are done now) // and that we can therefore reuse the client (but are done now)
return; return;
} }
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); tokio::spawn(self.did_change_workspace(vec![workspace], Vec::new()));
} }
#[allow(clippy::type_complexity, clippy::too_many_arguments)] #[allow(clippy::type_complexity, clippy::too_many_arguments)]
@ -179,8 +168,8 @@ impl Client {
args: &[String], args: &[String],
config: Option<Value>, config: Option<Value>,
server_environment: HashMap<String, String>, server_environment: HashMap<String, String>,
root_path: PathBuf, root: Option<PathBuf>,
root_uri: Option<lsp::Url>, workspace: PathBuf,
id: LanguageServerId, id: LanguageServerId,
name: String, name: String,
req_timeout: u64, req_timeout: u64,
@ -212,10 +201,13 @@ impl Client {
let (server_rx, server_tx, initialize_notify) = let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id, name.clone()); Transport::start(reader, writer, stderr, id, name.clone());
let workspace_folders = root_uri let workspace_folders = root.clone().into_iter().collect();
.clone() let root_uri = root.clone().map(|root| {
.map(|root| vec![workspace_for_uri(root)]) lsp::Url::from_file_path(root).expect("absolute paths can be converted to `Url`s")
.unwrap_or_default(); });
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.unwrap_or(workspace);
let client = Self { let client = Self {
id, id,
@ -376,10 +368,12 @@ impl Client {
self.config.as_ref() self.config.as_ref()
} }
pub async fn workspace_folders( pub async fn workspace_folders(&self) -> Vec<lsp::WorkspaceFolder> {
&self, self.workspace_folders
) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> { .lock()
self.workspace_folders.lock() .iter()
.map(|path| workspace_for_path(path))
.collect()
} }
/// Execute a RPC request on the language server. /// Execute a RPC request on the language server.
@ -526,7 +520,7 @@ impl Client {
#[allow(deprecated)] #[allow(deprecated)]
let params = lsp::InitializeParams { let params = lsp::InitializeParams {
process_id: Some(std::process::id()), process_id: Some(std::process::id()),
workspace_folders: Some(self.workspace_folders.lock().clone()), workspace_folders: Some(self.workspace_folders().await),
// root_path is obsolete, but some clients like pyright still use it so we specify both. // root_path is obsolete, but some clients like pyright still use it so we specify both.
// clients will prefer _uri if possible // clients will prefer _uri if possible
root_path: self.root_path.to_str().map(|path| path.to_owned()), root_path: self.root_path.to_str().map(|path| path.to_owned()),

@ -955,12 +955,8 @@ fn start_client(
workspace_is_cwd, workspace_is_cwd,
); );
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
if let Some(globset) = &ls_config.required_root_patterns { if let Some(globset) = &ls_config.required_root_patterns {
let root_path = root.as_ref().unwrap_or(&workspace);
if !root_path if !root_path
.read_dir()? .read_dir()?
.flatten() .flatten()
@ -976,8 +972,8 @@ fn start_client(
&ls_config.args, &ls_config.args,
ls_config.config.clone(), ls_config.config.clone(),
ls_config.environment.clone(), ls_config.environment.clone(),
root_path, root,
root_uri, workspace,
id, id,
name, name,
ls_config.timeout, ls_config.timeout,

Loading…
Cancel
Save