From 73523ec50bafcdab9308fa51808bef074ae898bc Mon Sep 17 00:00:00 2001 From: Kevin Bravo <79945749+0bkevin@users.noreply.github.com> Date: Sat, 18 Jul 2026 04:04:17 -0400 Subject: [PATCH] lsp: Set workspace folder names (#60938) # Objective Fixes #60518. Zed sends an empty `name` for entries in `initialize.params.workspaceFolders`. Language servers such as basedpyright use this field to identify workspace service instances, resulting in an unnamed instance. ## Solution Derive each workspace folder name from its URI, preferring the decoded filesystem basename. For basename-less URIs, fall back to a non-empty path, URI path segment, or the full URI. Use the same construction for initialization parameters, dynamic workspace-folder notifications, and `workspace/workspaceFolders` responses so all LSP messages remain consistent. ## Testing - `cargo fmt --all -- --check` - `cargo test -p lsp` - `cargo check -p project --all-targets --all-features` - `./script/clippy -p lsp` - `./script/clippy -p project` Reproduced with basedpyright 1.39.9 over an LSP stdio session. Before this change it logged: ```text Starting service instance "" ``` With a derived workspace folder name it logged: ```text Starting service instance "zed-60518-project" ``` Tested on macOS. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed language servers receiving empty workspace folder names. --- crates/lsp/src/lsp.rs | 67 ++++++++++++++++++++------------- crates/project/src/lsp_store.rs | 7 +--- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 361c5665d2..31554edd34 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -57,6 +57,25 @@ pub const DEFAULT_LSP_REQUEST_TIMEOUT: Duration = /// The shutdown timeout for LSP servers (including Prettier/Copilot). const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); +pub fn workspace_folder_for_uri(uri: Uri) -> WorkspaceFolder { + let name = uri + .to_file_path() + .ok() + .map(|path| { + let name = path.file_name().unwrap_or(path.as_os_str()); + name.to_string_lossy().into_owned() + }) + .filter(|name| !name.is_empty()) + .or_else(|| { + uri.path_segments() + .and_then(|mut segments| segments.rfind(|segment| !segment.is_empty())) + .map(str::to_owned) + }) + .unwrap_or_else(|| uri.as_str().to_owned()); + + WorkspaceFolder { uri, name } +} + type NotificationHandler = Box, Value, &mut AsyncApp)>; type PendingRespondTasks = Arc>>>; type ResponseHandler = Box) -> Task<()>>; @@ -747,21 +766,13 @@ impl LanguageServer { cx: &App, ) -> InitializeParams { let workspace_folders = self.workspace_folders.as_ref().map_or_else( - || { - vec![WorkspaceFolder { - name: Default::default(), - uri: self.root_uri.clone(), - }] - }, + || vec![workspace_folder_for_uri(self.root_uri.clone())], |folders| { folders .lock() .iter() .cloned() - .map(|uri| WorkspaceFolder { - name: Default::default(), - uri, - }) + .map(workspace_folder_for_uri) .collect() }, ); @@ -1625,10 +1636,7 @@ impl LanguageServer { if is_new_folder { let params = DidChangeWorkspaceFoldersParams { event: WorkspaceFoldersChangeEvent { - added: vec![WorkspaceFolder { - uri, - name: String::default(), - }], + added: vec![workspace_folder_for_uri(uri)], removed: vec![], }, }; @@ -1660,10 +1668,7 @@ impl LanguageServer { let params = DidChangeWorkspaceFoldersParams { event: WorkspaceFoldersChangeEvent { added: vec![], - removed: vec![WorkspaceFolder { - uri, - name: String::default(), - }], + removed: vec![workspace_folder_for_uri(uri)], }, }; self.notify::(params).ok(); @@ -1678,18 +1683,14 @@ impl LanguageServer { let old_workspace_folders = std::mem::take(&mut *workspace_folders); let added: Vec<_> = folders .difference(&old_workspace_folders) - .map(|uri| WorkspaceFolder { - uri: uri.clone(), - name: String::default(), - }) + .cloned() + .map(workspace_folder_for_uri) .collect(); let removed: Vec<_> = old_workspace_folders .difference(&folders) - .map(|uri| WorkspaceFolder { - uri: uri.clone(), - name: String::default(), - }) + .cloned() + .map(workspace_folder_for_uri) .collect(); *workspace_folders = folders; let should_notify = !added.is_empty() || !removed.is_empty(); @@ -2291,7 +2292,7 @@ mod tests { } #[gpui::test] - async fn test_initialize_params_has_root_path_and_root_uri(cx: &mut TestAppContext) { + async fn test_default_initialize_params(cx: &mut TestAppContext) { cx.update(|cx| { release_channel::init(semver::Version::new(0, 0, 0), cx); }); @@ -2306,6 +2307,9 @@ mod tests { Default::default(), &mut cx.to_async(), ); + let project_uri = Uri::from_file_path(std::env::temp_dir().join("my project")) + .expect("workspace folder URI should be valid"); + server.set_workspace_folders(BTreeSet::from_iter([project_uri.clone()])); let params = cx.update(|cx| server.default_initialize_params(false, false, cx)); @@ -2322,5 +2326,14 @@ mod tests { expected_path.to_string_lossy(), "root_path should be derived from root_uri" ); + let workspace_folders = params + .workspace_folders + .expect("workspace folders should be set"); + + let expected_workspace_folders = vec![WorkspaceFolder { + uri: project_uri, + name: "my project".to_string(), + }]; + assert_eq!(workspace_folders, expected_workspace_folders); } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index c1e5ca31f5..fee9666ec7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -99,7 +99,7 @@ use lsp::{ LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, - WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, + WorkDoneProgressCancelParams, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -984,10 +984,7 @@ impl LocalLspStore { let root = server.workspace_folders(); Ok(Some( root.into_iter() - .map(|uri| WorkspaceFolder { - uri, - name: Default::default(), - }) + .map(lsp::workspace_folder_for_uri) .collect(), )) }