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:
1 parent
ce3a79d607
commit
73523ec50b
2 files changed
+42
-32
No files matched your search
+40
-27
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user