lsp: Handle client/unregisterCapability to fix gopls (#12086)

This fixes #10224 by handling `client/unregisterCapability` requests
that have a `workspace/didChangeWatchedFiles` method.

While debugging the issue, I found out that `gopls` seems to block
indefinitely when there's no reply to the `client/unregisterCapability`
request. Even an empty response would fix the issue.

Seems like gopls 15.x and later seem to handle nested subfolders well,
but do not handle unanswered requests.

Instead of replying with an empty response, I decided to change how we
handle file watching and keep a list of all registered paths so that we
can then unregister paths and recreate the glob patterns.

Release Notes:

- Fixed `gopls` not working correctly when the `go.mod` file was in a
subfolder and not the root folder of the project opened in Zed.
([#10224](https://github.com/zed-industries/zed/issues/10224)).
This commit is contained in:
Thorsten Ball 2024-05-21 19:17:29 +02:00 committed by GitHub
parent 0563472832
commit b89f360199
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -55,9 +55,9 @@ use language::{
use log::error;
use lsp::{
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
DocumentHighlightKind, Edit, LanguageServer, LanguageServerBinary, LanguageServerId,
LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities, ServerHealthStatus,
ServerStatus, TextEdit,
DocumentHighlightKind, Edit, FileSystemWatcher, LanguageServer, LanguageServerBinary,
LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities,
ServerHealthStatus, ServerStatus, TextEdit,
};
use lsp_command::*;
use node_runtime::NodeRuntime;
@ -167,6 +167,8 @@ pub struct Project {
last_formatting_failure: Option<String>,
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
language_server_watched_paths: HashMap<LanguageServerId, HashMap<WorktreeId, GlobSet>>,
language_server_watcher_registrations:
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
client: Arc<client::Client>,
next_entry_id: Arc<AtomicUsize>,
join_project_response_message_id: u32,
@ -725,6 +727,7 @@ impl Project {
last_formatting_failure: None,
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: HashMap::default(),
language_server_watcher_registrations: HashMap::default(),
buffers_being_formatted: Default::default(),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
@ -875,6 +878,7 @@ impl Project {
last_formatting_failure: None,
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: HashMap::default(),
language_server_watcher_registrations: HashMap::default(),
opened_buffers: Default::default(),
buffers_being_formatted: Default::default(),
buffers_needing_diff: Default::default(),
@ -3471,7 +3475,7 @@ impl Project {
let options = serde_json::from_value(options)?;
this.update(&mut cx, |this, cx| {
this.on_lsp_did_change_watched_files(
server_id, options, cx,
server_id, &reg.id, options, cx,
);
})?;
}
@ -3483,6 +3487,27 @@ impl Project {
})
.detach();
language_server
.on_request::<lsp::request::UnregisterCapability, _, _>({
let this = this.clone();
move |params, mut cx| {
let this = this.clone();
async move {
for unreg in params.unregisterations.iter() {
if unreg.method == "workspace/didChangeWatchedFiles" {
this.update(&mut cx, |this, cx| {
this.on_lsp_unregister_did_change_watched_files(
server_id, &unreg.id, cx,
);
})?;
}
}
Ok(())
}
}
})
.detach();
language_server
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
let adapter = adapter.clone();
@ -4208,16 +4233,67 @@ impl Project {
fn on_lsp_did_change_watched_files(
&mut self,
language_server_id: LanguageServerId,
registration_id: &str,
params: DidChangeWatchedFilesRegistrationOptions,
cx: &mut ModelContext<Self>,
) {
let registrations = self
.language_server_watcher_registrations
.entry(language_server_id)
.or_default();
registrations.insert(registration_id.to_string(), params.watchers);
self.rebuild_watched_paths(language_server_id, cx);
}
fn on_lsp_unregister_did_change_watched_files(
&mut self,
language_server_id: LanguageServerId,
registration_id: &str,
cx: &mut ModelContext<Self>,
) {
let registrations = self
.language_server_watcher_registrations
.entry(language_server_id)
.or_default();
if registrations.remove(registration_id).is_some() {
log::info!(
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
registration_id
);
} else {
log::warn!(
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
language_server_id,
registration_id
);
}
self.rebuild_watched_paths(language_server_id, cx);
}
fn rebuild_watched_paths(
&mut self,
language_server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) {
let Some(watchers) = self
.language_server_watcher_registrations
.get(&language_server_id)
else {
return;
};
let watched_paths = self
.language_server_watched_paths
.entry(language_server_id)
.or_default();
let mut builders = HashMap::default();
for watcher in params.watchers {
for watcher in watchers.values().flatten() {
for worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade() {
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {