Add language server control tool into the status bar (#32490)

Release Notes:

- Added the language server control tool into the status bar

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Kirill Bulatov 2025-06-25 19:57:28 +03:00 committed by GitHub
parent 91c9281cea
commit c0acd8e8b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1992 additions and 312 deletions

View file

@ -783,7 +783,7 @@ impl BufferStore {
project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.get_by_path(&project_path, cx) {
if let Some(buffer) = self.get_by_path(&project_path) {
cx.emit(BufferStoreEvent::BufferOpened {
buffer: buffer.clone(),
project_path,
@ -946,7 +946,7 @@ impl BufferStore {
self.path_to_buffer_id.get(project_path)
}
pub fn get_by_path(&self, path: &ProjectPath, _cx: &App) -> Option<Entity<Buffer>> {
pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> {
self.path_to_buffer_id.get(path).and_then(|buffer_id| {
let buffer = self.get(*buffer_id);
buffer

View file

@ -275,7 +275,7 @@ impl BreakpointStore {
.context("Could not resolve provided abs path")?;
let buffer = this
.update(&mut cx, |this, cx| {
this.buffer_store().read(cx).get_by_path(&path, cx)
this.buffer_store().read(cx).get_by_path(&path)
})?
.context("Could not find buffer for a given path")?;
let breakpoint = message

View file

@ -3322,7 +3322,7 @@ impl Repository {
let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
continue;
};
if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
if let Some(buffer) = buffer_store.get_by_path(&project_path) {
if buffer
.read(cx)
.file()
@ -3389,7 +3389,7 @@ impl Repository {
let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
continue;
};
if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
if let Some(buffer) = buffer_store.get_by_path(&project_path) {
if buffer
.read(cx)
.file()
@ -3749,7 +3749,7 @@ impl Repository {
let buffer_id = git_store
.buffer_store
.read(cx)
.get_by_path(&project_path?, cx)?
.get_by_path(&project_path?)?
.read(cx)
.remote_id();
let diff_state = git_store.diffs.get(&buffer_id)?;

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
use ::serde::{Deserialize, Serialize};
use anyhow::Context as _;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use language::{LanguageServerStatusUpdate, ServerHealth};
use gpui::{App, Entity, Task, WeakEntity};
use language::ServerHealth;
use lsp::LanguageServer;
use rpc::proto;
use crate::{LspStore, Project, ProjectPath, lsp_store};
use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store};
pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc";
@ -36,24 +36,45 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
.on_notification::<ServerStatus, _>({
let name = name.clone();
move |params, cx| {
let status = params.message;
let log_message =
format!("Language server {name} (id {server_id}) status update: {status:?}");
match &params.health {
ServerHealth::Ok => log::info!("{log_message}"),
ServerHealth::Warning => log::warn!("{log_message}"),
ServerHealth::Error => log::error!("{log_message}"),
}
let message = params.message;
let log_message = message.as_ref().map(|message| {
format!("Language server {name} (id {server_id}) status update: {message}")
});
let status = match &params.health {
ServerHealth::Ok => {
if let Some(log_message) = log_message {
log::info!("{log_message}");
}
proto::ServerHealth::Ok
}
ServerHealth::Warning => {
if let Some(log_message) = log_message {
log::warn!("{log_message}");
}
proto::ServerHealth::Warning
}
ServerHealth::Error => {
if let Some(log_message) = log_message {
log::error!("{log_message}");
}
proto::ServerHealth::Error
}
};
lsp_store
.update(cx, |lsp_store, _| {
lsp_store.languages.update_lsp_status(
name.clone(),
LanguageServerStatusUpdate::Health(
params.health,
status.map(SharedString::from),
.update(cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id: server_id,
name: Some(name.clone()),
message: proto::update_language_server::Variant::StatusUpdate(
proto::StatusUpdate {
message,
status: Some(proto::status_update::Status::Health(
status as i32,
)),
},
),
);
});
})
.ok();
}

View file

@ -74,6 +74,7 @@ impl LanguageServerTreeNode {
pub(crate) fn server_id(&self) -> Option<LanguageServerId> {
self.0.upgrade()?.id.get().copied()
}
/// Returns a language server ID for this node if it has already been initialized; otherwise runs the provided closure to initialize the language server node in a tree.
/// May return None if the node no longer belongs to the server tree it was created in.
pub(crate) fn server_id_or_init(
@ -87,6 +88,11 @@ impl LanguageServerTreeNode {
.get_or_init(|| init(LaunchDisposition::from(&*this))),
)
}
/// Returns a language server name as the language server adapter would return.
pub fn name(&self) -> Option<LanguageServerName> {
self.0.upgrade().map(|node| node.name.clone())
}
}
impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {

View file

@ -81,7 +81,7 @@ use language::{
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
LanguageServerId, LanguageServerName, MessageActionItem,
LanguageServerId, LanguageServerName, LanguageServerSelector, MessageActionItem,
};
use lsp_command::*;
use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
@ -251,6 +251,7 @@ enum BufferOrderedMessage {
LanguageServerUpdate {
language_server_id: LanguageServerId,
message: proto::update_language_server::Variant,
name: Option<LanguageServerName>,
},
Resync,
}
@ -1790,7 +1791,7 @@ impl Project {
pub fn has_open_buffer(&self, path: impl Into<ProjectPath>, cx: &App) -> bool {
self.buffer_store
.read(cx)
.get_by_path(&path.into(), cx)
.get_by_path(&path.into())
.is_some()
}
@ -2500,7 +2501,7 @@ impl Project {
cx: &mut App,
) -> OpenLspBufferHandle {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, false, cx)
lsp_store.register_buffer_with_language_servers(&buffer, HashSet::default(), false, cx)
})
}
@ -2590,7 +2591,7 @@ impl Project {
}
pub fn get_open_buffer(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {
self.buffer_store.read(cx).get_by_path(path, cx)
self.buffer_store.read(cx).get_by_path(path)
}
fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) -> Result<()> {
@ -2640,7 +2641,7 @@ impl Project {
}
async fn send_buffer_ordered_messages(
this: WeakEntity<Self>,
project: WeakEntity<Self>,
rx: UnboundedReceiver<BufferOrderedMessage>,
cx: &mut AsyncApp,
) -> Result<()> {
@ -2677,7 +2678,7 @@ impl Project {
let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
while let Some(changes) = changes.next().await {
let is_local = this.read_with(cx, |this, _| this.is_local())?;
let is_local = project.read_with(cx, |this, _| this.is_local())?;
for change in changes {
match change {
@ -2697,7 +2698,7 @@ impl Project {
BufferOrderedMessage::Resync => {
operations_by_buffer_id.clear();
if this
if project
.update(cx, |this, cx| this.synchronize_remote_buffers(cx))?
.await
.is_ok()
@ -2709,9 +2710,10 @@ impl Project {
BufferOrderedMessage::LanguageServerUpdate {
language_server_id,
message,
name,
} => {
flush_operations(
&this,
&project,
&mut operations_by_buffer_id,
&mut needs_resync_with_host,
is_local,
@ -2719,12 +2721,14 @@ impl Project {
)
.await?;
this.read_with(cx, |this, _| {
if let Some(project_id) = this.remote_id() {
this.client
project.read_with(cx, |project, _| {
if let Some(project_id) = project.remote_id() {
project
.client
.send(proto::UpdateLanguageServer {
project_id,
language_server_id: language_server_id.0 as u64,
server_name: name.map(|name| String::from(name.0)),
language_server_id: language_server_id.to_proto(),
variant: Some(message),
})
.log_err();
@ -2735,7 +2739,7 @@ impl Project {
}
flush_operations(
&this,
&project,
&mut operations_by_buffer_id,
&mut needs_resync_with_host,
is_local,
@ -2856,12 +2860,14 @@ impl Project {
LspStoreEvent::LanguageServerUpdate {
language_server_id,
message,
name,
} => {
if self.is_local() {
self.enqueue_buffer_ordered_message(
BufferOrderedMessage::LanguageServerUpdate {
language_server_id: *language_server_id,
message: message.clone(),
name: name.clone(),
},
)
.ok();
@ -3140,20 +3146,22 @@ impl Project {
pub fn restart_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
only_restart_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
) {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.restart_language_servers_for_buffers(buffers, cx)
lsp_store.restart_language_servers_for_buffers(buffers, only_restart_servers, cx)
})
}
pub fn stop_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
also_restart_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
) {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.stop_language_servers_for_buffers(buffers, cx)
lsp_store.stop_language_servers_for_buffers(buffers, also_restart_servers, cx)
})
}

View file

@ -49,6 +49,10 @@ pub struct ProjectSettings {
#[serde(default)]
pub lsp: HashMap<LanguageServerName, LspSettings>,
/// Common language server settings.
#[serde(default)]
pub global_lsp_settings: GlobalLspSettings,
/// Configuration for Debugger-related features
#[serde(default)]
pub dap: HashMap<DebugAdapterName, DapSettings>,
@ -110,6 +114,16 @@ pub enum ContextServerSettings {
},
}
/// Common language server settings.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GlobalLspSettings {
/// Whether to show the LSP servers button in the status bar.
///
/// Default: `true`
#[serde(default = "default_true")]
pub button: bool,
}
impl ContextServerSettings {
pub fn default_extension() -> Self {
Self::Extension {
@ -271,6 +285,14 @@ impl Default for InlineDiagnosticsSettings {
}
}
impl Default for GlobalLspSettings {
fn default() -> Self {
Self {
button: default_true(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CargoDiagnosticsSettings {
/// When enabled, Zed disables rust-analyzer's check on save and starts to query

View file

@ -918,6 +918,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![rust_buffer.clone(), json_buffer.clone()],
HashSet::default(),
cx,
);
});
@ -1715,12 +1716,16 @@ 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], cx);
project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx);
});
let mut events = cx.events(&project);
// Simulate the newly started server sending more diagnostics.
let fake_server = fake_servers.next().await.unwrap();
assert_eq!(
events.next().await.unwrap(),
Event::LanguageServerRemoved(LanguageServerId(0))
);
assert_eq!(
events.next().await.unwrap(),
Event::LanguageServerAdded(
@ -1820,7 +1825,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()], cx);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
// The diagnostics are cleared.
@ -1875,7 +1880,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()], cx);
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
});
let mut fake_server = fake_servers.next().await.unwrap();