Allow to restart stopped servers (#65387)

Part of https://github.com/zed-industries/zed/issues/61896


https://github.com/user-attachments/assets/1625de1d-0ce3-4257-9f61-887ae86954be

Currently, if one stops language servers via the LSP button, there's no
way to bring them back.
The PR adjusts the "restart language server" command to work so that it
actually restarts the servers relevant to the file opened, even in this
case.

Release Notes:

- Allowed to restart stopped servers
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-10-14 16:09:28 +00:00
1 parent b25dc09370
commit 2dcc4cc04d
10 files changed
+562 -228

No files matched your search

@@ -14,7 +14,9 @@ use gpui::{
use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{ConfiguredLanguageServer, Formatter, FormatterList, LanguageSettings},
language_settings::{
ConfiguredLanguageServer, Formatter, FormatterList, LanguageSettings, all_language_settings,
},
rust_lang, tree_sitter_typescript,
};
use node_runtime::NodeRuntime;
@@ -33,7 +35,7 @@ use rpc::proto;
use serde_json::json;
use settings::{
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
SettingsStore,
SemanticTokens, SettingsStore,
};
use std::{
path::{Path, PathBuf},
@@ -939,7 +941,7 @@ async fn test_ssh_restarting_language_server_replaces_remote_status(
});
project_a.update(cx_a, |project, cx| {
project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), true, cx);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
let restarted_server = fake_language_servers.next().await.unwrap();
@@ -979,6 +981,53 @@ async fn test_ssh_restarting_language_server_replaces_remote_status(
"restarting a remote language server should replace the old log store entry"
);
});
let lsp_store = project_a.read_with(cx_a, |project, _| project.lsp_store());
lsp_store.update(cx_a, |lsp_store, cx| {
lsp_store.stop_all_language_servers(cx);
});
executor.run_until_parked();
lsp_store.read_with(cx_a, |lsp_store, _| {
assert_eq!(lsp_store.language_server_statuses().count(), 0);
});
cx_a.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.semantic_tokens = Some(SemanticTokens::Full);
});
});
executor.run_until_parked();
server_cx.read(|cx| {
assert_eq!(
all_language_settings(None, cx).defaults.semantic_tokens,
SemanticTokens::Full
);
});
lsp_store.read_with(cx_a, |lsp_store, _| {
assert_eq!(lsp_store.language_server_statuses().count(), 0);
});
project_a.update(cx_a, |project, cx| {
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
executor.run_until_parked();
lsp_store.read_with(cx_a, |lsp_store, _| {
assert_eq!(lsp_store.language_server_statuses().count(), 1);
});
let mut resumed_server = fake_language_servers.next().await.unwrap();
assert_ne!(resumed_server.server.server_id(), restarted_server_id);
assert_eq!(
resumed_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Uri::from_file_path(path!("/project/a.rs")).unwrap(),
language_id: "rust".to_owned(),
version: 0,
text: "fn main() {}".to_owned(),
}
);
}
#[gpui::test]
-1
View File
@@ -8693,7 +8693,6 @@ impl Editor {
project.restart_language_servers_for_buffers(
multi_buffer.all_buffers().into_iter().collect(),
HashSet::default(),
true,
cx,
);
});
+73
View File
@@ -39254,6 +39254,79 @@ async fn test_dynamic_document_highlight_registration_refreshes_editor(cx: &mut
);
}
#[gpui::test]
async fn test_restart_language_server_action_after_stop(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state("fn main() {}ˇ");
let project = cx.update_editor(|editor, _, _| editor.project.clone().unwrap());
let lsp_store = cx.read(|cx| project.read(cx).lsp_store());
let server_name = cx.lsp.server.name();
let mut server_id = cx.lsp.server.server_id();
let (shutdown_sender, mut shutdown_receiver) = mpsc::unbounded();
let mut fake_servers = cx.read(|cx| {
project.read(cx).languages().register_fake_lsp_server(
server_name,
lsp::ServerCapabilities::default(),
Some(Box::new(move |fake_server| {
let shutdown_sender = shutdown_sender.clone();
fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
let (reply_sender, reply_receiver) = oneshot::channel::<()>();
shutdown_sender.unbounded_send(reply_sender).unwrap();
async move {
reply_receiver.await?;
Ok(())
}
});
})),
)
});
for stop_all in [false, true] {
if stop_all {
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| lsp_store.stop_all_language_servers(cx));
});
shutdown_receiver.next().await.unwrap().send(()).unwrap();
} else {
cx.dispatch_action(StopLanguageServer);
}
cx.run_until_parked();
cx.read(|cx| assert_eq!(lsp_store.read(cx).language_server_statuses().count(), 0));
cx.dispatch_action(RestartLanguageServer);
cx.run_until_parked();
cx.read(|cx| assert_eq!(lsp_store.read(cx).language_server_statuses().count(), 1));
let mut restarted_server = fake_servers.next().await.unwrap();
assert_ne!(restarted_server.server.server_id(), server_id);
server_id = restarted_server.server.server_id();
let document = restarted_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document;
assert_eq!(document.uri, cx.buffer_lsp_url);
assert_eq!(document.text, "fn main() {}");
}
let mut events = cx.events(&project);
cx.dispatch_action(RestartLanguageServer);
let reply_sender = shutdown_receiver.next().await.unwrap();
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| lsp_store.stop_all_language_servers(cx));
});
reply_sender.send(()).unwrap();
cx.run_until_parked();
cx.read(|cx| assert_eq!(lsp_store.read(cx).language_server_statuses().count(), 0));
assert!(fake_servers.next().now_or_never().is_none());
let removed_servers = std::iter::from_fn(|| events.next().now_or_never().flatten())
.filter_map(|event| match event {
project::Event::LanguageServerRemoved(id) => Some(id),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(removed_servers, vec![server_id]);
}
#[gpui::test]
async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -1202,12 +1202,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
// Start a new instance of the language server.
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![buffer.clone()],
HashSet::default(),
true,
cx,
)
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
});
cx.executor().run_until_parked();
@@ -1248,12 +1243,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![buffer.clone()],
HashSet::default(),
true,
cx,
)
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
});
// The extension re-fetches the latest version of the language server.
-1
View File
@@ -572,7 +572,6 @@ impl LanguageServerState {
HashSet::from_iter([LanguageServerSelector::Name(
server_name_for_restart.clone(),
)]),
true,
cx,
);
})
+193 -155
View File
@@ -357,8 +357,9 @@ pub struct LocalLspStore {
HashMap<Option<SharedString>, HashMap<PathBuf, Option<SharedString>>>,
>,
restricted_worktrees_tasks: HashMap<WorktreeId, (Subscription, watch::Receiver<bool>)>,
all_language_servers_stopped: bool,
stopped_language_servers: HashSet<LanguageServerName>,
auto_start_language_servers: bool,
language_server_start_overrides: HashMap<LanguageServerName, bool>,
language_server_restart_tasks: Vec<Task<()>>,
buffers_to_refresh_hash_set: HashSet<BufferId>,
buffers_to_refresh_queue: VecDeque<BufferId>,
@@ -380,6 +381,12 @@ fn workspace_folder_uri(worktree: &Worktree, path: &RelPath) -> Option<Uri> {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LanguageServerStartMode {
Automatic,
Explicit,
}
impl LocalLspStore {
/// Returns the running language server for the given ID. Note if the language server is starting, it will not be returned.
pub fn running_language_server_for_id(
@@ -394,14 +401,22 @@ impl LocalLspStore {
}
}
fn can_start_language_server(&self, name: &LanguageServerName) -> bool {
self.language_server_start_overrides
.get(name)
.copied()
.unwrap_or(self.auto_start_language_servers)
}
fn get_or_insert_language_server(
&mut self,
worktree_handle: &Entity<Worktree>,
delegate: Arc<LocalLspAdapterDelegate>,
disposition: &Arc<LaunchDisposition>,
language_name: &LanguageName,
start_mode: LanguageServerStartMode,
cx: &mut App,
) -> LanguageServerId {
) -> Option<LanguageServerId> {
let key = LanguageServerSeed {
worktree_id: worktree_handle.read(cx).id(),
name: disposition.server_name.clone(),
@@ -411,10 +426,19 @@ impl LocalLspStore {
},
toolchain: disposition.toolchain.clone(),
};
if start_mode == LanguageServerStartMode::Explicit {
self.language_server_start_overrides
.insert(key.name.clone(), true);
}
if let Some(state) = self.language_server_ids.get_mut(&key) {
state.project_roots.insert(disposition.path.path.clone());
state.id
Some(state.id)
} else {
if start_mode == LanguageServerStartMode::Automatic
&& !self.can_start_language_server(&key.name)
{
return None;
}
let adapter = self
.languages
.lsp_adapters(language_name)
@@ -438,7 +462,7 @@ impl LocalLspStore {
"Expected `start_language_server` to ensure that `key` exists in a map"
);
}
new_language_server_id
Some(new_language_server_id)
}
}
@@ -3139,11 +3163,9 @@ impl LocalLspStore {
&mut self,
buffer_handle: &Entity<Buffer>,
only_register_servers: HashSet<LanguageServerSelector>,
start_mode: LanguageServerStartMode,
cx: &mut Context<LspStore>,
) {
if self.all_language_servers_stopped {
return;
}
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id();
@@ -3188,9 +3210,9 @@ impl LocalLspStore {
return;
};
let language_name = language.name();
let (reused, delegate, servers) = self
let (delegate, servers) = self
.reuse_existing_language_server(&self.lsp_tree, &worktree, &language_name, cx)
.map(|(delegate, apply)| (true, delegate, apply(&mut self.lsp_tree)))
.map(|(delegate, apply)| (delegate, apply(&mut self.lsp_tree)))
.unwrap_or_else(|| {
let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
let delegate: Arc<dyn ManifestDelegate> =
@@ -3206,19 +3228,11 @@ impl LocalLspStore {
cx,
)
.collect::<Vec<_>>();
(false, lsp_delegate, servers)
(lsp_delegate, servers)
});
let servers_and_adapters = servers
.into_iter()
.filter_map(|server_node| {
if reused && server_node.server_id().is_none() {
return None;
}
if let Some(name) = server_node.name()
&& self.stopped_language_servers.contains(&name)
{
return None;
}
if !only_register_servers.is_empty() {
if let Some(server_id) = server_node.server_id()
&& !only_register_servers.contains(&LanguageServerSelector::Id(server_id))
@@ -3243,15 +3257,16 @@ impl LocalLspStore {
delegate.clone(),
disposition,
&language_name,
start_mode,
cx,
);
)?;
if let Some(state) = self.language_servers.get(&server_id)
&& let Some(workspace_folder) = workspace_folder
{
state.add_workspace_folder(workspace_folder);
};
server_id
Some(server_id)
}
})?;
let server_state = self.language_servers.get(&server_id)?;
@@ -3351,7 +3366,12 @@ impl LocalLspStore {
.map(move |(_, (server_node, server_languages))| {
(worktree_id, server_node, server_languages)
})
.filter(|(_, _, server_languages)| server_languages.contains(language_name))
.filter(|(_, node, server_languages)| {
server_languages.contains(language_name)
&& node
.id()
.is_some_and(|id| self.language_servers.contains_key(&id))
})
.map(|(worktree_id, server_node, _)| {
(
*worktree_id,
@@ -4961,8 +4981,9 @@ impl LspStore {
buffer_pull_diagnostics_result_ids: HashMap::default(),
workspace_pull_diagnostics_result_ids: HashMap::default(),
restricted_worktrees_tasks: HashMap::default(),
all_language_servers_stopped: false,
stopped_language_servers: HashSet::default(),
auto_start_language_servers: true,
language_server_start_overrides: HashMap::default(),
language_server_restart_tasks: Vec::new(),
watched_manifest_filenames: ManifestProvidersStore::global(cx)
.manifest_file_names(),
}),
@@ -5081,7 +5102,12 @@ impl LspStore {
if let Some(local) = self.as_local_mut() {
local.initialize_buffer(buffer, cx);
if local.registered_buffers.contains_key(&buffer_id) {
local.register_buffer_with_language_servers(buffer, HashSet::default(), cx);
local.register_buffer_with_language_servers(
buffer,
HashSet::default(),
LanguageServerStartMode::Automatic,
cx,
);
}
}
}
@@ -5312,16 +5338,13 @@ impl LspStore {
&mut self,
buffer: &Entity<Buffer>,
only_register_servers: HashSet<LanguageServerSelector>,
ignore_refcounts: bool,
cx: &mut Context<Self>,
) -> OpenLspBufferHandle {
let buffer_id = buffer.read(cx).remote_id();
let handle = OpenLspBufferHandle(cx.new(|_| OpenLspBuffer(buffer.clone())));
if let Some(local) = self.as_local_mut() {
let refcount = local.registered_buffers.entry(buffer_id).or_insert(0);
if !ignore_refcounts {
*refcount += 1;
}
*refcount += 1;
// We run early exits on non-existing buffers AFTER we mark the buffer as registered in order to handle buffer saving.
// When a new unnamed buffer is created and saved, we will start loading it's language. Once the language is loaded, we go over all "language-less" buffers and try to fit that new language
@@ -5334,69 +5357,70 @@ impl LspStore {
return handle;
}
if ignore_refcounts || *refcount == 1 {
local.register_buffer_with_language_servers(buffer, only_register_servers, cx);
if *refcount == 1 {
local.register_buffer_with_language_servers(
buffer,
only_register_servers,
LanguageServerStartMode::Automatic,
cx,
);
}
if !ignore_refcounts {
cx.observe_release(&handle.0, move |lsp_store, buffer, cx| {
let refcount = {
let local = lsp_store.as_local_mut().unwrap();
let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else {
debug_panic!("bad refcounting");
return;
};
*refcount -= 1;
*refcount
cx.observe_release(&handle.0, move |lsp_store, buffer, cx| {
let refcount = {
let local = lsp_store.as_local_mut().unwrap();
let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else {
debug_panic!("bad refcounting");
return;
};
if refcount == 0 {
lsp_store.lsp_data.remove(&buffer_id);
lsp_store.buffer_reload_tasks.remove(&buffer_id);
let local = lsp_store.as_local_mut().unwrap();
local.registered_buffers.remove(&buffer_id);
local.buffers_opened_in_servers.remove(&buffer_id);
if let Some(file) = File::from_dyn(buffer.0.read(cx).file()).cloned() {
local.unregister_old_buffer_from_language_servers(&buffer.0, &file, cx);
*refcount -= 1;
*refcount
};
if refcount == 0 {
lsp_store.lsp_data.remove(&buffer_id);
lsp_store.buffer_reload_tasks.remove(&buffer_id);
let local = lsp_store.as_local_mut().unwrap();
local.registered_buffers.remove(&buffer_id);
let buffer_abs_path = file.abs_path(cx);
for (_, buffer_pull_diagnostics_result_ids) in
&mut local.buffer_pull_diagnostics_result_ids
{
buffer_pull_diagnostics_result_ids.retain(
|_, buffer_result_ids| {
buffer_result_ids.remove(&buffer_abs_path);
!buffer_result_ids.is_empty()
},
);
}
local.buffers_opened_in_servers.remove(&buffer_id);
if let Some(file) = File::from_dyn(buffer.0.read(cx).file()).cloned() {
local.unregister_old_buffer_from_language_servers(&buffer.0, &file, cx);
// Closing the buffer does not change which servers are relevant
// to its path, so pushed diagnostics stay: a running server may
// still describe the file. Pulled document diagnostics need an
// open document to be refreshed, so they are dropped, except for
// servers whose workspace pull still owns the path's verdict.
let servers_to_clear = local
.language_servers
.keys()
.copied()
.filter(|server_id| {
!local.server_pulls_workspace_diagnostics(*server_id)
})
.collect::<Vec<_>>();
lsp_store.clear_path_diagnostics_for_servers(
buffer_abs_path,
servers_to_clear,
|_, diagnostic, _| {
diagnostic.source_kind != DiagnosticSourceKind::Pulled
},
cx,
);
let buffer_abs_path = file.abs_path(cx);
for (_, buffer_pull_diagnostics_result_ids) in
&mut local.buffer_pull_diagnostics_result_ids
{
buffer_pull_diagnostics_result_ids.retain(|_, buffer_result_ids| {
buffer_result_ids.remove(&buffer_abs_path);
!buffer_result_ids.is_empty()
});
}
// Closing the buffer does not change which servers are relevant
// to its path, so pushed diagnostics stay: a running server may
// still describe the file. Pulled document diagnostics need an
// open document to be refreshed, so they are dropped, except for
// servers whose workspace pull still owns the path's verdict.
let servers_to_clear = local
.language_servers
.keys()
.copied()
.filter(|server_id| {
!local.server_pulls_workspace_diagnostics(*server_id)
})
.collect::<Vec<_>>();
lsp_store.clear_path_diagnostics_for_servers(
buffer_abs_path,
servers_to_clear,
|_, diagnostic, _| {
diagnostic.source_kind != DiagnosticSourceKind::Pulled
},
cx,
);
}
})
.detach();
}
}
})
.detach();
} else if let Some((upstream_client, upstream_project_id)) = self.upstream_client() {
let buffer_id = buffer.read(cx).remote_id().to_proto();
cx.background_spawn(async move {
@@ -5518,6 +5542,7 @@ impl LspStore {
local.register_buffer_with_language_servers(
&buffer,
HashSet::default(),
LanguageServerStartMode::Automatic,
cx,
);
}
@@ -5536,6 +5561,7 @@ impl LspStore {
local.register_buffer_with_language_servers(
&buffer,
HashSet::default(),
LanguageServerStartMode::Automatic,
cx,
);
}
@@ -5685,7 +5711,12 @@ impl LspStore {
if let Some(local) = self.as_local_mut()
&& local.registered_buffers.contains_key(&buffer_id)
{
local.register_buffer_with_language_servers(buffer_entity, HashSet::default(), cx);
local.register_buffer_with_language_servers(
buffer_entity,
HashSet::default(),
LanguageServerStartMode::Automatic,
cx,
);
}
Some(worktree.read(cx).id())
} else {
@@ -6354,16 +6385,13 @@ impl LspStore {
.semantic_token_config
.update_global_mode(new_global_semantic_tokens_mode)
{
let all_stopped = self
.as_local()
.is_some_and(|local| local.all_language_servers_stopped);
if !all_stopped {
// Restart servers without clearing per-server stopped status.
// Individually-stopped servers will be skipped by the guard in
// register_buffer_with_language_servers.
let buffers = self.buffer_store.read(cx).buffers().collect();
self.restart_language_servers_for_buffers(buffers, HashSet::default(), false, cx);
}
let buffers = self.buffer_store.read(cx).buffers().collect();
self.restart_local_language_servers_for_buffers(
buffers,
HashSet::default(),
LanguageServerStartMode::Automatic,
cx,
);
}
cx.notify();
@@ -6374,10 +6402,6 @@ impl LspStore {
let Some(local) = self.as_local_mut() else {
return;
};
if local.all_language_servers_stopped {
return;
}
let stopped_language_servers = local.stopped_language_servers.clone();
let mut adapters = BTreeMap::default();
let get_adapter = {
let languages = local.languages.clone();
@@ -6460,11 +6484,6 @@ impl LspStore {
)
.collect::<Vec<_>>();
for node in nodes {
if let Some(name) = node.name()
&& stopped_language_servers.contains(&name)
{
continue;
}
let server_id = node.server_id_or_init(|disposition| {
let path = &disposition.path;
let workspace_folder =
@@ -6485,21 +6504,24 @@ impl LspStore {
language.name(),
),
};
local.language_server_ids.remove(&key);
if local.can_start_language_server(&key.name) {
local.language_server_ids.remove(&key);
}
let server_id = local.get_or_insert_language_server(
&worktree,
lsp_delegate.clone(),
disposition,
&language.name(),
LanguageServerStartMode::Automatic,
cx,
);
)?;
if let Some(state) = local.language_servers.get(&server_id)
&& let Some(workspace_folder) = workspace_folder
{
state.add_workspace_folder(workspace_folder);
};
server_id
Some(server_id)
});
if let Some(language_server_id) = server_id {
@@ -11250,7 +11272,6 @@ impl LspStore {
})
})
.collect(),
false,
cx,
);
// Pull diagnostics for the buffer even if it was already registered.
@@ -12438,7 +12459,6 @@ impl LspStore {
})
})
.collect(),
true,
cx,
);
});
@@ -12991,7 +13011,8 @@ impl LspStore {
pub fn stop_all_language_servers(&mut self, cx: &mut Context<Self>) {
if let Some(local) = self.as_local_mut() {
local.all_language_servers_stopped = true;
local.auto_start_language_servers = false;
local.language_server_start_overrides.clear();
}
self.shutdown_all_language_servers(cx).detach();
}
@@ -13011,6 +13032,7 @@ impl LspStore {
let Some(local) = self.as_local_mut() else {
return Task::ready(());
};
local.language_server_restart_tasks.clear();
let language_servers_to_stop = local
.language_server_ids
.values()
@@ -13029,19 +13051,17 @@ impl LspStore {
pub fn restart_all_language_servers(&mut self, cx: &mut Context<Self>) {
if let Some(local) = self.as_local_mut() {
local.all_language_servers_stopped = false;
local.auto_start_language_servers = true;
local.language_server_start_overrides.clear();
}
// `restart_language_servers_for_buffers` with empty selectors and `clear_stopped`
// clears `stopped_language_servers` for us.
let buffers = self.buffer_store.read(cx).buffers().collect();
self.restart_language_servers_for_buffers(buffers, HashSet::default(), true, cx);
self.restart_language_servers_for_buffers(buffers, HashSet::default(), cx);
}
pub fn restart_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
only_restart_servers: HashSet<LanguageServerSelector>,
clear_stopped: bool,
cx: &mut Context<Self>,
) {
if let Some((client, project_id)) = self.upstream_client() {
@@ -13075,44 +13095,12 @@ impl LspStore {
});
cx.background_spawn(request).detach_and_log_err(cx);
} else {
let (stopped_names, stop_task) = if only_restart_servers.is_empty() {
self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx)
} else {
self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx)
};
cx.spawn(async move |lsp_store, cx| {
stop_task.await;
lsp_store.update(cx, |lsp_store, cx| {
if clear_stopped {
if let Some(local) = lsp_store.as_local_mut() {
if only_restart_servers.is_empty() {
// A full restart of these buffers un-suppresses every
// manually-stopped server, even ones that are no longer
// running (and so weren't returned in `stopped_names`).
local.stopped_language_servers.clear();
} else {
for name in &stopped_names {
local.stopped_language_servers.remove(name);
}
for selector in &only_restart_servers {
if let LanguageServerSelector::Name(name) = selector {
local.stopped_language_servers.remove(name);
}
}
}
}
}
for buffer in buffers {
lsp_store.register_buffer_with_language_servers(
&buffer,
only_restart_servers.clone(),
true,
cx,
);
}
})
})
.detach();
self.restart_local_language_servers_for_buffers(
buffers,
only_restart_servers,
LanguageServerStartMode::Explicit,
cx,
);
}
}
@@ -13159,7 +13147,9 @@ impl LspStore {
let (stopped_names, task) =
self.stop_local_language_servers_for_buffers(&buffers, also_stop_servers, cx);
if let Some(local) = self.as_local_mut() {
local.stopped_language_servers.extend(stopped_names);
local
.language_server_start_overrides
.extend(stopped_names.into_iter().map(|name| (name, false)));
}
cx.background_spawn(async move {
task.await;
@@ -13168,6 +13158,54 @@ impl LspStore {
}
}
fn restart_local_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
only_restart_servers: HashSet<LanguageServerSelector>,
start_mode: LanguageServerStartMode,
cx: &mut Context<Self>,
) {
let Some(local) = self.as_local_mut() else {
return;
};
local
.language_server_restart_tasks
.retain(|task| !task.is_ready());
let (_, stop_task) = if only_restart_servers.is_empty() {
self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx)
} else {
self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx)
};
let stop_task = stop_task.shared();
cx.background_spawn(stop_task.clone()).detach();
let task = cx.spawn(async move |lsp_store, cx| {
stop_task.await;
lsp_store
.update(cx, |lsp_store, cx| {
let Some(local) = lsp_store.as_local_mut() else {
return;
};
for buffer in buffers {
if local
.registered_buffers
.contains_key(&buffer.read(cx).remote_id())
{
local.register_buffer_with_language_servers(
&buffer,
only_restart_servers.clone(),
start_mode,
cx,
);
}
}
})
.ok();
});
if let Some(local) = self.as_local_mut() {
local.language_server_restart_tasks.push(task);
}
}
fn stop_local_language_servers_for_buffers(
&mut self,
buffers: &[Entity<Buffer>],
+39 -21
View File
@@ -7,7 +7,7 @@
//! to reuse existing language server.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, btree_map::Entry},
sync::{Arc, Weak},
};
@@ -71,10 +71,14 @@ impl LanguageServerTreeNode {
/// May return None if the node no longer belongs to the server tree it was created in.
pub(crate) fn server_id_or_init(
&self,
init: impl FnOnce(&Arc<LaunchDisposition>) -> LanguageServerId,
init: impl FnOnce(&Arc<LaunchDisposition>) -> Option<LanguageServerId>,
) -> Option<LanguageServerId> {
let this = self.0.upgrade()?;
Some(*this.id.get_or_init(|| init(&this.disposition)))
let node = self.0.upgrade()?;
if let Some(server_id) = node.id.get() {
return Some(*server_id);
}
let server_id = init(&node.disposition)?;
Some(*node.id.get_or_init(|| server_id))
}
/// Returns a language server name as the language server adapter would return.
@@ -179,23 +183,29 @@ impl LanguageServerTree {
.entry(root_path.path.clone())
.or_default()
.entry(adapter.name());
let (node, languages) = inner_node.or_insert_with(|| {
let new_node = || {
let toolchain = self.toolchains.read(cx).active_toolchain(
root_path.worktree_id,
&root_path.path,
language_name.clone(),
);
(
Arc::new(InnerTreeNode::new(
adapter.name(),
root_path.clone(),
settings.clone(),
toolchain,
)),
Default::default(),
)
});
Arc::new(InnerTreeNode::new(
adapter.name(),
root_path.clone(),
settings,
toolchain,
))
};
let (node, languages) = match inner_node {
Entry::Occupied(mut entry) => {
if entry.get().0.id.get().is_none() {
entry.get_mut().0 = new_node();
}
entry.into_mut()
}
Entry::Vacant(entry) => entry.insert((new_node(), BTreeSet::new())),
};
languages.insert(language_name.clone());
Arc::downgrade(node).into()
})
@@ -342,6 +352,11 @@ impl LanguageServerTree {
.entry(RelPath::empty_arc())
.or_default()
.entry(node.disposition.server_name.clone())
.and_modify(|(existing, _)| {
if existing.id().is_none() {
*existing = node.clone();
}
})
.or_insert_with(|| (node, BTreeSet::new()))
.1
.insert(language_name);
@@ -353,9 +368,6 @@ pub(crate) struct ServerTreeRebase {
new_tree: LanguageServerTree,
/// All server IDs seen in the old tree.
all_server_ids: BTreeMap<LanguageServerId, LanguageServerName>,
/// Server IDs we've preserved for a new iteration of the tree. `all_server_ids - rebased_server_ids` is the
/// set of server IDs that can be shut down.
rebased_server_ids: BTreeSet<LanguageServerId>,
}
impl ServerTreeRebase {
@@ -385,7 +397,6 @@ impl ServerTreeRebase {
old_contents,
all_server_ids,
new_tree,
rebased_server_ids: BTreeSet::new(),
}
}
@@ -434,7 +445,6 @@ impl ServerTreeRebase {
return Some(node);
};
if let Some(existing_id) = existing_node.id.get() {
self.rebased_server_ids.insert(*existing_id);
live_node.id.set(*existing_id).ok();
}
@@ -449,11 +459,19 @@ impl ServerTreeRebase {
LanguageServerTree,
BTreeMap<LanguageServerId, LanguageServerName>,
) {
let preserved_server_ids = self
.new_tree
.instances
.values()
.flat_map(|servers| servers.roots.values())
.flat_map(|nodes| nodes.values())
.filter_map(|(node, _)| node.id())
.collect::<BTreeSet<_>>();
(
self.new_tree,
self.all_server_ids
.into_iter()
.filter(|(id, _)| !self.rebased_server_ids.contains(id))
.filter(|(id, _)| !preserved_server_ids.contains(id))
.collect(),
)
}
+2 -8
View File
@@ -3329,7 +3329,7 @@ impl Project {
cx: &mut App,
) -> OpenLspBufferHandle {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(buffer, HashSet::default(), false, cx)
lsp_store.register_buffer_with_language_servers(buffer, HashSet::default(), cx)
})
}
@@ -4200,16 +4200,10 @@ impl Project {
&mut self,
buffers: Vec<Entity<Buffer>>,
only_restart_servers: HashSet<LanguageServerSelector>,
clear_stopped: bool,
cx: &mut Context<Self>,
) {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.restart_language_servers_for_buffers(
buffers,
only_restart_servers,
clear_stopped,
cx,
)
lsp_store.restart_language_servers_for_buffers(buffers, only_restart_servers, cx)
})
}
+54 -7
View File
@@ -5,7 +5,7 @@ use std::{
time::{Duration, Instant},
};
use collections::HashMap;
use collections::{HashMap, HashSet};
use fs::{FakeFs, Fs};
use futures::{FutureExt, StreamExt};
use gpui::{Entity, TestAppContext, UpdateGlobal as _};
@@ -168,7 +168,7 @@ async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let (_visible_buffer, _visible_handle) = project
let (visible_buffer, _visible_handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/main.rs"), cx)
})
@@ -186,13 +186,10 @@ async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut
.unwrap()
.0
});
let external_uri = Uri::from_file_path(path!("/the-registry/dep/src/dep.rs")).unwrap();
let external_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer_via_lsp(
Uri::from_file_path(path!("/the-registry/dep/src/dep.rs")).unwrap(),
server_id,
cx,
)
project.open_local_buffer_via_lsp(external_uri.clone(), server_id, cx)
})
.await
.unwrap();
@@ -226,6 +223,56 @@ async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut
);
assert!(!lsp_store.has_language_server_seed_for_worktree(invisible_worktree_id));
});
let external_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer_via_lsp(external_uri.clone(), server_id, cx)
})
.await
.unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |store, cx| store.stop_all_language_servers(cx));
cx.run_until_parked();
let _external_handle = project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&external_buffer, cx)
});
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(vec![visible_buffer], HashSet::default(), cx);
});
cx.run_until_parked();
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 1);
});
let mut fake_server = fake_servers.next().await.unwrap();
assert_ne!(fake_server.server.server_id(), server_id);
let document = fake_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.now_or_never()
.expect("visible buffer was not reopened")
.text_document;
assert_eq!(
document.uri,
Uri::from_file_path(path!("/the-root/main.rs")).unwrap()
);
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![external_buffer.clone()],
HashSet::default(),
cx,
);
});
cx.run_until_parked();
let document = fake_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.now_or_never()
.expect("external buffer was not reopened")
.text_document;
assert_eq!(document.uri, external_uri);
assert_eq!(document.text, "pub fn dep() {}");
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 1);
});
assert!(fake_servers.next().now_or_never().is_none());
}
#[gpui::test]
+147 -20
View File
@@ -1767,6 +1767,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
json!({
"test.rs": "const A: i32 = 1;",
"test2.rs": "",
"later.rs": "fn later() {}",
"Cargo.toml": "a = 1",
"package.json": "{\"a\": 1}",
}),
@@ -1817,6 +1818,26 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
},
);
let (initialize_sender, initialize_receiver) = async_channel::unbounded();
let mut replacement_rust_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "replacement-rust-server",
opt_in_languages: HashSet::from_iter([LanguageName::new_static("Rust")]),
initializer: Some(Box::new(move |server| {
let initialize_sender = initialize_sender.clone();
server.set_request_handler::<lsp::request::Initialize, _, _>(move |params, _| {
initialize_sender.try_send(params).unwrap();
future::ready(Ok(lsp::InitializeResult {
capabilities: lsp::LanguageServer::full_capabilities(),
server_info: None,
}))
});
})),
..FakeLspAdapter::default()
},
);
// Open a buffer without an associated language server.
let (toml_buffer, _handle) = project
.update(cx, |project, cx| {
@@ -2090,7 +2111,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
project.restart_language_servers_for_buffers(
vec![rust_buffer.clone(), json_buffer.clone()],
HashSet::default(),
true,
cx,
);
});
@@ -2159,6 +2179,128 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.await,
close_message,
);
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| lsp_store.stop_all_language_servers(cx));
cx.run_until_parked();
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 0)
});
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{"semantic_tokens":"combined","languages":{"Rust":{"language_servers":["replacement-rust-server"]}}}"#,
cx,
)
.unwrap();
});
cx.run_until_parked();
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 0)
});
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![rust_buffer.clone()],
HashSet::default(),
cx,
);
});
cx.run_until_parked();
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 1)
});
let mut replacement_rust_server = replacement_rust_servers.next().await.unwrap();
assert_eq!(
initialize_receiver
.try_recv()
.unwrap()
.initialization_options,
None
);
let _json_handle = project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&json_buffer, cx)
});
for (path, text) in [
(path!("/dir/test.rs"), "const A: i32 = 12;"),
(path!("/dir/later.rs"), "fn later() {}"),
] {
let _registration = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path, cx)
})
.await
.unwrap();
assert_eq!(
replacement_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Uri::from_file_path(path).unwrap(),
version: 0,
text: text.to_owned(),
language_id: "rust".to_owned(),
}
);
}
let mut server_id = replacement_rust_server.server.server_id();
for semantic_tokens in ["combined", "full"] {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
&json!({
"semantic_tokens": semantic_tokens,
"languages": { "Rust": { "language_servers": ["replacement-rust-server"] } },
"lsp": { "replacement-rust-server": { "initialization_options": { "check": true } } }
})
.to_string(),
cx,
)
.unwrap();
});
cx.run_until_parked();
lsp_store.read_with(cx, |store, _| {
assert_eq!(store.language_server_statuses().count(), 1);
});
let server = replacement_rust_servers
.next()
.now_or_never()
.flatten()
.expect("resumed server was not restarted after changing settings");
assert_ne!(server.server.server_id(), server_id);
server_id = server.server.server_id();
let params = initialize_receiver.try_recv().unwrap();
assert_eq!(
params.initialization_options,
Some(json!({ "check": true }))
);
assert_eq!(
params
.capabilities
.text_document
.unwrap()
.semantic_tokens
.unwrap()
.augments_syntax_tokens,
Some(semantic_tokens == "combined")
);
}
for servers in [
&mut fake_rust_servers,
&mut fake_json_servers,
&mut replacement_rust_servers,
] {
assert_eq!(
servers
.next()
.now_or_never()
.flatten()
.map(|server| server.server.name()),
None,
);
}
}
#[gpui::test]
@@ -3298,7 +3440,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
// Restart the server before the diagnostics finish updating.
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), true, cx);
project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx);
});
let mut events = cx.events(&project);
@@ -3416,12 +3558,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
});
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![buffer.clone()],
HashSet::default(),
true,
cx,
);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
// The diagnostics are cleared.
@@ -3476,12 +3613,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
});
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![buffer.clone()],
HashSet::default(),
true,
cx,
);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
let mut fake_server = fake_servers.next().await.unwrap();
@@ -4995,12 +5127,7 @@ async fn test_diagnostic_summaries_cleared_on_server_restart(cx: &mut gpui::Test
let mut events = cx.events(&project);
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![buffer.clone()],
HashSet::default(),
true,
cx,
);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
cx.executor().run_until_parked();