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.
This commit is contained in:
Kevin Bravo authored and GitHub committed 2026-07-18 08:04:17 +00:00
1 parent ce3a79d607
commit 73523ec50b
2 files changed
+42 -32

No files matched your search

+40 -27
View File
@@ -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<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
type PendingRespondTasks = Arc<Mutex<HashMap<RequestId, Task<()>>>>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>) -> 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::<DidChangeWorkspaceFolders>(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);
}
}
+2 -5
View File
@@ -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(),
))
}