Fix race when language server registers for workspace/didChangeWatchedFiles (#9177)

This fixes #8896 by storing the `watched_paths` in a separate HashMap,
allowing us to handle the request even before we mark the language
server as running.

Downside is that we have yet another data structure for language
servers, but it also makes the `Running` enum case a bit smaller.

And it fixes the race condition.

Release Notes:

- Fixed language servers not being notified of file changes if language
server registers for file-notification right after starting up.
([#8896](https://github.com/zed-industries/zed/issues/8896)).

Co-authored-by: Bennet <bennetbo@gmx.de>
Co-authored-by: Remco <djsmits12@gmail.com>
This commit is contained in:
Thorsten Ball 2024-03-11 16:30:30 +01:00 committed by GitHub
parent 0be20d0817
commit f066dd268f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -131,6 +131,7 @@ pub struct Project {
language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
language_server_watched_paths: HashMap<LanguageServerId, HashMap<WorktreeId, GlobSet>>,
client: Arc<client::Client>,
next_entry_id: Arc<AtomicUsize>,
join_project_response_message_id: u32,
@ -305,7 +306,6 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
watched_paths: HashMap<WorktreeId, GlobSet>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@ -601,6 +601,7 @@ impl Project {
language_server_ids: HashMap::default(),
language_server_statuses: Default::default(),
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: HashMap::default(),
buffers_being_formatted: Default::default(),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
@ -732,6 +733,7 @@ impl Project {
})
.collect(),
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: HashMap::default(),
opened_buffers: Default::default(),
buffers_being_formatted: Default::default(),
buffers_needing_diff: Default::default(),
@ -3317,7 +3319,6 @@ impl Project {
LanguageServerState::Running {
adapter: adapter.clone(),
language: language.clone(),
watched_paths: Default::default(),
server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
},
@ -3462,13 +3463,14 @@ impl Project {
}
}
self.language_server_watched_paths.remove(&server_id);
self.language_server_statuses.remove(&server_id);
cx.notify();
let server_state = self.language_servers.remove(&server_id);
cx.emit(Event::LanguageServerRemoved(server_id));
cx.spawn(move |this, cx| async move {
Self::shutdown_language_server(this, server_state, name, server_id, cx).await;
cx.spawn(move |_, cx| async move {
Self::shutdown_language_server(server_state, name, cx).await;
orphaned_worktrees
})
} else {
@ -3477,11 +3479,9 @@ impl Project {
}
async fn shutdown_language_server(
this: WeakModel<Project>,
server_state: Option<LanguageServerState>,
name: Arc<str>,
server_id: LanguageServerId,
mut cx: AsyncAppContext,
cx: AsyncAppContext,
) {
let server = match server_state {
Some(LanguageServerState::Starting(task)) => {
@ -3512,14 +3512,6 @@ impl Project {
shutdown.await;
}
}
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.language_server_statuses.remove(&server_id);
cx.notify();
})
.ok();
}
}
pub fn restart_language_servers_for_buffers(
@ -3890,64 +3882,63 @@ impl Project {
params: DidChangeWatchedFilesRegistrationOptions,
cx: &mut ModelContext<Self>,
) {
if let Some(LanguageServerState::Running { watched_paths, .. }) =
self.language_servers.get_mut(&language_server_id)
{
let mut builders = HashMap::default();
for watcher in params.watchers {
for worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade() {
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
if let Some(abs_path) = tree.abs_path().to_str() {
let relative_glob_pattern = match &watcher.glob_pattern {
lsp::GlobPattern::String(s) => s
.strip_prefix(abs_path)
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)),
lsp::GlobPattern::Relative(rp) => {
let base_uri = match &rp.base_uri {
lsp::OneOf::Left(workspace_folder) => {
&workspace_folder.uri
}
lsp::OneOf::Right(base_uri) => base_uri,
};
base_uri.to_file_path().ok().and_then(|file_path| {
(file_path.to_str() == Some(abs_path))
.then_some(rp.pattern.as_str())
})
}
};
if let Some(relative_glob_pattern) = relative_glob_pattern {
let literal_prefix = glob_literal_prefix(relative_glob_pattern);
tree.as_local_mut()
.unwrap()
.add_path_prefix_to_scan(Path::new(literal_prefix).into());
if let Some(glob) = Glob::new(relative_glob_pattern).log_err() {
builders
.entry(tree.id())
.or_insert_with(|| GlobSetBuilder::new())
.add(glob);
}
return true;
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 worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade() {
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
if let Some(abs_path) = tree.abs_path().to_str() {
let relative_glob_pattern = match &watcher.glob_pattern {
lsp::GlobPattern::String(s) => s
.strip_prefix(abs_path)
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)),
lsp::GlobPattern::Relative(rp) => {
let base_uri = match &rp.base_uri {
lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri,
lsp::OneOf::Right(base_uri) => base_uri,
};
base_uri.to_file_path().ok().and_then(|file_path| {
(file_path.to_str() == Some(abs_path))
.then_some(rp.pattern.as_str())
})
}
};
if let Some(relative_glob_pattern) = relative_glob_pattern {
let literal_prefix = glob_literal_prefix(relative_glob_pattern);
tree.as_local_mut()
.unwrap()
.add_path_prefix_to_scan(Path::new(literal_prefix).into());
if let Some(glob) = Glob::new(relative_glob_pattern).log_err() {
builders
.entry(tree.id())
.or_insert_with(|| GlobSetBuilder::new())
.add(glob);
}
return true;
}
false
});
if glob_is_inside_worktree {
break;
}
false
});
if glob_is_inside_worktree {
break;
}
}
}
watched_paths.clear();
for (worktree_id, builder) in builders {
if let Ok(globset) = builder.build() {
watched_paths.insert(worktree_id, globset);
}
}
cx.notify();
}
watched_paths.clear();
for (worktree_id, builder) in builders {
if let Ok(globset) = builder.build() {
watched_paths.insert(worktree_id, globset);
}
}
cx.notify();
}
async fn on_lsp_workspace_edit(
@ -6731,6 +6722,8 @@ impl Project {
self.language_server_ids
.remove(&(id_to_remove, server_name));
self.language_server_statuses.remove(&server_id_to_remove);
self.language_server_watched_paths
.remove(&server_id_to_remove);
self.last_workspace_edits_by_language_server
.remove(&server_id_to_remove);
self.language_servers.remove(&server_id_to_remove);
@ -6985,13 +6978,14 @@ impl Project {
let abs_path = worktree_handle.read(cx).abs_path();
for server_id in &language_server_ids {
if let Some(LanguageServerState::Running {
server,
watched_paths,
..
}) = self.language_servers.get(server_id)
if let Some(LanguageServerState::Running { server, .. }) =
self.language_servers.get(server_id)
{
if let Some(watched_paths) = watched_paths.get(&worktree_id) {
if let Some(watched_paths) = self
.language_server_watched_paths
.get(&server_id)
.and_then(|paths| paths.get(&worktree_id))
{
let params = lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
@ -7013,7 +7007,6 @@ impl Project {
})
.collect(),
};
if !params.changes.is_empty() {
server
.notify::<lsp::notification::DidChangeWatchedFiles>(params)