Detailed changes
@@ -14,6 +14,7 @@ dependencies = [
"gpui",
"language",
"project",
+ "proto",
"release_channel",
"smallvec",
"ui",
@@ -9025,6 +9026,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
+ "picker",
"project",
"release_channel",
"serde_json",
@@ -41,7 +41,8 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
- "ctrl-shift-i": "edit_prediction::ToggleMenu"
+ "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -47,7 +47,8 @@
"fn-f": "zed::ToggleFullScreen",
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
- "ctrl-cmd-i": "edit_prediction::ToggleMenu"
+ "ctrl-cmd-i": "edit_prediction::ToggleMenu",
+ "ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -1720,6 +1720,11 @@
// }
// }
},
+ // Common language server settings.
+ "global_lsp_settings": {
+ // Whether to show the LSP servers button in the status bar.
+ "button": true
+ },
// Jupyter settings
"jupyter": {
"enabled": true
@@ -21,6 +21,7 @@ futures.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
+proto.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
@@ -80,10 +80,13 @@ impl ActivityIndicator {
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
- while let Some((name, status)) = status_events.next().await {
+ while let Some((name, binary_status)) = status_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
- this.statuses.push(ServerStatus { name, status });
+ this.statuses.push(ServerStatus {
+ name,
+ status: LanguageServerStatusUpdate::Binary(binary_status),
+ });
cx.notify();
})?;
}
@@ -112,8 +115,76 @@ impl ActivityIndicator {
cx.subscribe(
&project.read(cx).lsp_store(),
- |_, _, event, cx| match event {
- LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
+ |activity_indicator, _, event, cx| match event {
+ LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
+ if let proto::update_language_server::Variant::StatusUpdate(status_update) =
+ message
+ {
+ let Some(name) = name.clone() else {
+ return;
+ };
+ let status = match &status_update.status {
+ Some(proto::status_update::Status::Binary(binary_status)) => {
+ if let Some(binary_status) =
+ proto::ServerBinaryStatus::from_i32(*binary_status)
+ {
+ let binary_status = match binary_status {
+ proto::ServerBinaryStatus::None => BinaryStatus::None,
+ proto::ServerBinaryStatus::CheckingForUpdate => {
+ BinaryStatus::CheckingForUpdate
+ }
+ proto::ServerBinaryStatus::Downloading => {
+ BinaryStatus::Downloading
+ }
+ proto::ServerBinaryStatus::Starting => {
+ BinaryStatus::Starting
+ }
+ proto::ServerBinaryStatus::Stopping => {
+ BinaryStatus::Stopping
+ }
+ proto::ServerBinaryStatus::Stopped => {
+ BinaryStatus::Stopped
+ }
+ proto::ServerBinaryStatus::Failed => {
+ let Some(error) = status_update.message.clone()
+ else {
+ return;
+ };
+ BinaryStatus::Failed { error }
+ }
+ };
+ LanguageServerStatusUpdate::Binary(binary_status)
+ } else {
+ return;
+ }
+ }
+ Some(proto::status_update::Status::Health(health_status)) => {
+ if let Some(health) =
+ proto::ServerHealth::from_i32(*health_status)
+ {
+ let health = match health {
+ proto::ServerHealth::Ok => ServerHealth::Ok,
+ proto::ServerHealth::Warning => ServerHealth::Warning,
+ proto::ServerHealth::Error => ServerHealth::Error,
+ };
+ LanguageServerStatusUpdate::Health(
+ health,
+ status_update.message.clone().map(SharedString::from),
+ )
+ } else {
+ return;
+ }
+ }
+ None => return,
+ };
+
+ activity_indicator.statuses.retain(|s| s.name != name);
+ activity_indicator
+ .statuses
+ .push(ServerStatus { name, status });
+ }
+ cx.notify()
+ }
_ => {}
},
)
@@ -228,9 +299,23 @@ impl ActivityIndicator {
_: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(updater) = &self.auto_updater {
- updater.update(cx, |updater, cx| updater.dismiss_error(cx));
+ let error_dismissed = if let Some(updater) = &self.auto_updater {
+ updater.update(cx, |updater, cx| updater.dismiss_error(cx))
+ } else {
+ false
+ };
+ if error_dismissed {
+ return;
}
+
+ self.project.update(cx, |project, cx| {
+ if project.last_formatting_failure(cx).is_some() {
+ project.reset_last_formatting_failure(cx);
+ true
+ } else {
+ false
+ }
+ });
}
fn pending_language_server_work<'a>(
@@ -399,6 +484,12 @@ impl ActivityIndicator {
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
for status in &self.statuses {
match &status.status {
+ LanguageServerStatusUpdate::Binary(
+ BinaryStatus::Starting | BinaryStatus::Stopping,
+ ) => {}
+ LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
+ servers_to_clear_statuses.insert(status.name.clone());
+ }
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
checking_for_update.push(status.name.clone());
}
@@ -2008,6 +2008,7 @@ async fn join_project(
session.connection_id,
proto::UpdateLanguageServer {
project_id: project_id.to_proto(),
+ server_name: Some(language_server.name.clone()),
language_server_id: language_server.id,
variant: Some(
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
@@ -16164,7 +16164,7 @@ impl Editor {
})
}
- fn restart_language_server(
+ pub fn restart_language_server(
&mut self,
_: &RestartLanguageServer,
_: &mut Window,
@@ -16175,6 +16175,7 @@ impl Editor {
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
multi_buffer.all_buffers().into_iter().collect(),
+ HashSet::default(),
cx,
);
});
@@ -16182,7 +16183,7 @@ impl Editor {
}
}
- fn stop_language_server(
+ pub fn stop_language_server(
&mut self,
_: &StopLanguageServer,
_: &mut Window,
@@ -16193,6 +16194,7 @@ impl Editor {
project.update(cx, |project, cx| {
project.stop_language_servers_for_buffers(
multi_buffer.all_buffers().into_iter().collect(),
+ HashSet::default(),
cx,
);
cx.emit(project::Event::RefreshInlayHints);
@@ -4,13 +4,13 @@ use crate::{
GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION, SchemaVersion,
};
use async_compression::futures::bufread::GzipEncoder;
-use collections::BTreeMap;
+use collections::{BTreeMap, HashSet};
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs, RealFs};
use futures::{AsyncReadExt, StreamExt, io::BufReader};
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use http_client::{FakeHttpClient, Response};
-use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate};
+use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@@ -720,20 +720,22 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
status_updates.next().await.unwrap(),
status_updates.next().await.unwrap(),
status_updates.next().await.unwrap(),
+ status_updates.next().await.unwrap(),
],
[
(
LanguageServerName::new_static("gleam"),
- LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate)
+ BinaryStatus::Starting
),
(
LanguageServerName::new_static("gleam"),
- LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading)
+ BinaryStatus::CheckingForUpdate
),
(
LanguageServerName::new_static("gleam"),
- LanguageServerStatusUpdate::Binary(BinaryStatus::None)
- )
+ BinaryStatus::Downloading
+ ),
+ (LanguageServerName::new_static("gleam"), BinaryStatus::None)
]
);
@@ -794,7 +796,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()], cx)
+ project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
});
cx.executor().run_until_parked();
@@ -816,7 +818,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
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)
});
// The extension re-fetches the latest version of the language server.
@@ -413,10 +413,6 @@ impl PickerDelegate for BranchListDelegate {
cx.emit(DismissEvent);
}
- fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
- None
- }
-
fn render_match(
&self,
ix: usize,
@@ -1,6 +1,4 @@
-use gpui::{
- AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
-};
+use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{Project, git_store::Repository};
@@ -207,15 +205,6 @@ impl PickerDelegate for RepositorySelectorDelegate {
.ok();
}
- fn render_header(
- &self,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) -> Option<AnyElement> {
- // TODO: Implement header rendering if needed
- None
- }
-
fn render_match(
&self,
ix: usize,
@@ -157,6 +157,9 @@ pub enum BinaryStatus {
None,
CheckingForUpdate,
Downloading,
+ Starting,
+ Stopping,
+ Stopped,
Failed { error: String },
}
@@ -248,7 +251,7 @@ pub struct LanguageQueries {
#[derive(Clone, Default)]
struct ServerStatusSender {
- txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerStatusUpdate)>>>>,
+ txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, BinaryStatus)>>>>,
}
pub struct LoadedLanguage {
@@ -1085,11 +1088,7 @@ impl LanguageRegistry {
self.state.read().all_lsp_adapters.get(name).cloned()
}
- pub fn update_lsp_status(
- &self,
- server_name: LanguageServerName,
- status: LanguageServerStatusUpdate,
- ) {
+ pub fn update_lsp_binary_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
self.lsp_binary_status_tx.send(server_name, status);
}
@@ -1145,7 +1144,7 @@ impl LanguageRegistry {
pub fn language_server_binary_statuses(
&self,
- ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerStatusUpdate)> {
+ ) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> {
self.lsp_binary_status_tx.subscribe()
}
@@ -1260,15 +1259,13 @@ impl LanguageRegistryState {
}
impl ServerStatusSender {
- fn subscribe(
- &self,
- ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerStatusUpdate)> {
+ fn subscribe(&self) -> mpsc::UnboundedReceiver<(LanguageServerName, BinaryStatus)> {
let (tx, rx) = mpsc::unbounded();
self.txs.lock().push(tx);
rx
}
- fn send(&self, name: LanguageServerName, status: LanguageServerStatusUpdate) {
+ fn send(&self, name: LanguageServerName, status: BinaryStatus) {
let mut txs = self.txs.lock();
txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
}
@@ -12,8 +12,8 @@ use fs::Fs;
use futures::{Future, FutureExt};
use gpui::AsyncApp;
use language::{
- BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageServerStatusUpdate,
- LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
+ BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
+ LspAdapter, LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
use serde::Serialize;
@@ -82,10 +82,8 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy {
language_server_id: LanguageServerName,
status: BinaryStatus,
) {
- self.language_registry.update_lsp_status(
- language_server_id,
- LanguageServerStatusUpdate::Binary(status),
- );
+ self.language_registry
+ .update_lsp_binary_status(language_server_id, status);
}
}
@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
+client.workspace = true
collections.workspace = true
copilot.workspace = true
editor.workspace = true
@@ -22,18 +23,19 @@ gpui.workspace = true
itertools.workspace = true
language.workspace = true
lsp.workspace = true
+picker.workspace = true
project.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
tree-sitter.workspace = true
ui.workspace = true
+util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
-client = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
gpui = { workspace = true, features = ["test-support"] }
@@ -1,17 +1,54 @@
mod key_context_view;
mod lsp_log;
+pub mod lsp_tool;
mod syntax_tree_view;
#[cfg(test)]
mod lsp_log_tests;
-use gpui::App;
+use gpui::{App, AppContext, Entity};
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
+use ui::{Context, Window};
+use workspace::{Item, ItemHandle, SplitDirection, Workspace};
pub fn init(cx: &mut App) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
key_context_view::init(cx);
}
+
+fn get_or_create_tool<T>(
+ workspace: &mut Workspace,
+ destination: SplitDirection,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ new_tool: impl FnOnce(&mut Window, &mut Context<T>) -> T,
+) -> Entity<T>
+where
+ T: Item,
+{
+ if let Some(item) = workspace.item_of_type::<T>(cx) {
+ return item;
+ }
+
+ let new_tool = cx.new(|cx| new_tool(window, cx));
+ match workspace.find_pane_in_direction(destination, cx) {
+ Some(right_pane) => {
+ workspace.add_item(
+ right_pane,
+ new_tool.boxed_clone(),
+ None,
+ true,
+ true,
+ window,
+ cx,
+ );
+ }
+ None => {
+ workspace.split_item(destination, new_tool.boxed_clone(), window, cx);
+ }
+ }
+ new_tool
+}
@@ -3,14 +3,14 @@ use copilot::Copilot;
use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
use futures::{StreamExt, channel::mpsc};
use gpui::{
- AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
- ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
+ AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, Global,
+ IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
};
use itertools::Itertools;
use language::{LanguageServerId, language_settings::SoftWrap};
use lsp::{
- IoKind, LanguageServer, LanguageServerName, MessageType, SetTraceParams, TraceValue,
- notification::SetTrace,
+ IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
+ SetTraceParams, TraceValue, notification::SetTrace,
};
use project::{Project, WorktreeId, search::SearchQuery};
use std::{any::TypeId, borrow::Cow, sync::Arc};
@@ -21,6 +21,8 @@ use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
};
+use crate::get_or_create_tool;
+
const SEND_LINE: &str = "\n// Send:";
const RECEIVE_LINE: &str = "\n// Receive:";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
@@ -44,7 +46,7 @@ trait Message: AsRef<str> {
}
}
-struct LogMessage {
+pub(super) struct LogMessage {
message: String,
typ: MessageType,
}
@@ -71,7 +73,7 @@ impl Message for LogMessage {
}
}
-struct TraceMessage {
+pub(super) struct TraceMessage {
message: String,
}
@@ -99,7 +101,7 @@ impl Message for RpcMessage {
type Level = ();
}
-struct LanguageServerState {
+pub(super) struct LanguageServerState {
name: Option<LanguageServerName>,
worktree_id: Option<WorktreeId>,
kind: LanguageServerKind,
@@ -204,8 +206,13 @@ pub(crate) struct LogMenuItem {
actions!(dev, [OpenLanguageServerLogs]);
+pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
+
+impl Global for GlobalLogStore {}
+
pub fn init(cx: &mut App) {
let log_store = cx.new(LogStore::new);
+ cx.set_global(GlobalLogStore(log_store.downgrade()));
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
let project = workspace.project();
@@ -219,13 +226,14 @@ pub fn init(cx: &mut App) {
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
let project = workspace.project().read(cx);
if project.is_local() || project.is_via_ssh() {
- workspace.split_item(
+ let project = workspace.project().clone();
+ let log_store = log_store.clone();
+ get_or_create_tool(
+ workspace,
SplitDirection::Right,
- Box::new(cx.new(|cx| {
- LspLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
- })),
window,
cx,
+ move |window, cx| LspLogView::new(project, log_store, window, cx),
);
}
});
@@ -354,7 +362,7 @@ impl LogStore {
);
}
- fn get_language_server_state(
+ pub(super) fn get_language_server_state(
&mut self,
id: LanguageServerId,
) -> Option<&mut LanguageServerState> {
@@ -480,11 +488,14 @@ impl LogStore {
cx.notify();
}
- fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
+ pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
Some(&self.language_servers.get(&server_id)?.log_messages)
}
- fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
+ pub(super) fn server_trace(
+ &self,
+ server_id: LanguageServerId,
+ ) -> Option<&VecDeque<TraceMessage>> {
Some(&self.language_servers.get(&server_id)?.trace_messages)
}
@@ -529,6 +540,110 @@ impl LogStore {
Some(())
}
+ pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
+ match server {
+ LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
+ LanguageServerSelector::Name(name) => self
+ .language_servers
+ .iter()
+ .any(|(_, state)| state.name.as_ref() == Some(name)),
+ }
+ }
+
+ pub fn open_server_log(
+ &mut self,
+ workspace: WeakEntity<Workspace>,
+ server: LanguageServerSelector,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ cx.spawn_in(window, async move |log_store, cx| {
+ let Some(log_store) = log_store.upgrade() else {
+ return;
+ };
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ let project = workspace.project().clone();
+ let tool_log_store = log_store.clone();
+ let log_view = get_or_create_tool(
+ workspace,
+ SplitDirection::Right,
+ window,
+ cx,
+ move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
+ );
+ log_view.update(cx, |log_view, cx| {
+ let server_id = match server {
+ LanguageServerSelector::Id(id) => Some(id),
+ LanguageServerSelector::Name(name) => {
+ log_store.read(cx).language_servers.iter().find_map(
+ |(id, state)| {
+ if state.name.as_ref() == Some(&name) {
+ Some(*id)
+ } else {
+ None
+ }
+ },
+ )
+ }
+ };
+ if let Some(server_id) = server_id {
+ log_view.show_logs_for_server(server_id, window, cx);
+ }
+ });
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ pub fn open_server_trace(
+ &mut self,
+ workspace: WeakEntity<Workspace>,
+ server: LanguageServerSelector,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ cx.spawn_in(window, async move |log_store, cx| {
+ let Some(log_store) = log_store.upgrade() else {
+ return;
+ };
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ let project = workspace.project().clone();
+ let tool_log_store = log_store.clone();
+ let log_view = get_or_create_tool(
+ workspace,
+ SplitDirection::Right,
+ window,
+ cx,
+ move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
+ );
+ log_view.update(cx, |log_view, cx| {
+ let server_id = match server {
+ LanguageServerSelector::Id(id) => Some(id),
+ LanguageServerSelector::Name(name) => {
+ log_store.read(cx).language_servers.iter().find_map(
+ |(id, state)| {
+ if state.name.as_ref() == Some(&name) {
+ Some(*id)
+ } else {
+ None
+ }
+ },
+ )
+ }
+ };
+ if let Some(server_id) = server_id {
+ log_view.show_rpc_trace_for_server(server_id, window, cx);
+ }
+ });
+ })
+ .ok();
+ })
+ .detach();
+ }
+
fn on_io(
&mut self,
language_server_id: LanguageServerId,
@@ -856,7 +971,7 @@ impl LspLogView {
self.editor_subscriptions = editor_subscriptions;
cx.notify();
}
- window.focus(&self.focus_handle);
+ self.editor.read(cx).focus_handle(cx).focus(window);
}
fn update_log_level(
@@ -882,7 +997,7 @@ impl LspLogView {
cx.notify();
}
- window.focus(&self.focus_handle);
+ self.editor.read(cx).focus_handle(cx).focus(window);
}
fn show_trace_for_server(
@@ -904,7 +1019,7 @@ impl LspLogView {
self.editor_subscriptions = editor_subscriptions;
cx.notify();
}
- window.focus(&self.focus_handle);
+ self.editor.read(cx).focus_handle(cx).focus(window);
}
fn show_rpc_trace_for_server(
@@ -947,7 +1062,7 @@ impl LspLogView {
cx.notify();
}
- window.focus(&self.focus_handle);
+ self.editor.read(cx).focus_handle(cx).focus(window);
}
fn toggle_rpc_trace_for_server(
@@ -1011,7 +1126,7 @@ impl LspLogView {
self.editor = editor;
self.editor_subscriptions = editor_subscriptions;
cx.notify();
- window.focus(&self.focus_handle);
+ self.editor.read(cx).focus_handle(cx).focus(window);
}
}
@@ -0,0 +1,917 @@
+use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
+
+use client::proto;
+use collections::{HashMap, HashSet};
+use editor::{Editor, EditorEvent};
+use gpui::{Corner, DismissEvent, Entity, Focusable as _, Subscription, Task, WeakEntity, actions};
+use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
+use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
+use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
+use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
+use settings::{Settings as _, SettingsStore};
+use ui::{Context, IconButtonShape, Indicator, Tooltip, Window, prelude::*};
+
+use workspace::{StatusItemView, Workspace};
+
+use crate::lsp_log::GlobalLogStore;
+
+actions!(lsp_tool, [ToggleMenu]);
+
+pub struct LspTool {
+ state: Entity<PickerState>,
+ lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
+ _subscriptions: Vec<Subscription>,
+}
+
+struct PickerState {
+ workspace: WeakEntity<Workspace>,
+ lsp_store: WeakEntity<LspStore>,
+ active_editor: Option<ActiveEditor>,
+ language_servers: LanguageServers,
+}
+
+#[derive(Debug)]
+struct LspPickerDelegate {
+ state: Entity<PickerState>,
+ selected_index: usize,
+ items: Vec<LspItem>,
+ other_servers_start_index: Option<usize>,
+}
+
+struct ActiveEditor {
+ editor: WeakEntity<Editor>,
+ _editor_subscription: Subscription,
+ editor_buffers: HashSet<BufferId>,
+}
+
+#[derive(Debug, Default, Clone)]
+struct LanguageServers {
+ health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
+ binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
+ servers_per_buffer_abs_path:
+ HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
+}
+
+#[derive(Debug, Clone)]
+struct LanguageServerHealthStatus {
+ name: LanguageServerName,
+ health: Option<(Option<SharedString>, ServerHealth)>,
+}
+
+#[derive(Debug, Clone)]
+struct LanguageServerBinaryStatus {
+ status: BinaryStatus,
+ message: Option<SharedString>,
+}
+
+impl LanguageServerHealthStatus {
+ fn health(&self) -> Option<ServerHealth> {
+ self.health.as_ref().map(|(_, health)| *health)
+ }
+
+ fn message(&self) -> Option<SharedString> {
+ self.health
+ .as_ref()
+ .and_then(|(message, _)| message.clone())
+ }
+}
+
+impl LspPickerDelegate {
+ fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
+ self.state.update(cx, |state, cx| {
+ let editor_buffers = state
+ .active_editor
+ .as_ref()
+ .map(|active_editor| active_editor.editor_buffers.clone())
+ .unwrap_or_default();
+ let editor_buffer_paths = editor_buffers
+ .iter()
+ .filter_map(|buffer_id| {
+ let buffer_path = state
+ .lsp_store
+ .update(cx, |lsp_store, cx| {
+ Some(
+ project::File::from_dyn(
+ lsp_store
+ .buffer_store()
+ .read(cx)
+ .get(*buffer_id)?
+ .read(cx)
+ .file(),
+ )?
+ .abs_path(cx),
+ )
+ })
+ .ok()??;
+ Some(buffer_path)
+ })
+ .collect::<Vec<_>>();
+
+ let mut servers_with_health_checks = HashSet::default();
+ let mut server_ids_with_health_checks = HashSet::default();
+ let mut buffer_servers =
+ Vec::with_capacity(state.language_servers.health_statuses.len());
+ let mut other_servers =
+ Vec::with_capacity(state.language_servers.health_statuses.len());
+ let buffer_server_ids = editor_buffer_paths
+ .iter()
+ .filter_map(|buffer_path| {
+ state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .get(buffer_path)
+ })
+ .flatten()
+ .fold(HashMap::default(), |mut acc, (server_id, name)| {
+ match acc.entry(*server_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ let old_name: &mut Option<&LanguageServerName> = o.get_mut();
+ if old_name.is_none() {
+ *old_name = name.as_ref();
+ }
+ }
+ hash_map::Entry::Vacant(v) => {
+ v.insert(name.as_ref());
+ }
+ }
+ acc
+ });
+ for (server_id, server_state) in &state.language_servers.health_statuses {
+ let binary_status = state
+ .language_servers
+ .binary_statuses
+ .get(&server_state.name);
+ servers_with_health_checks.insert(&server_state.name);
+ server_ids_with_health_checks.insert(*server_id);
+ if buffer_server_ids.contains_key(server_id) {
+ buffer_servers.push(ServerData::WithHealthCheck(
+ *server_id,
+ server_state,
+ binary_status,
+ ));
+ } else {
+ other_servers.push(ServerData::WithHealthCheck(
+ *server_id,
+ server_state,
+ binary_status,
+ ));
+ }
+ }
+
+ for (server_name, status) in state
+ .language_servers
+ .binary_statuses
+ .iter()
+ .filter(|(name, _)| !servers_with_health_checks.contains(name))
+ {
+ let has_matching_server = state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .iter()
+ .filter(|(path, _)| editor_buffer_paths.contains(path))
+ .flat_map(|(_, server_associations)| server_associations.iter())
+ .any(|(_, name)| name.as_ref() == Some(server_name));
+ if has_matching_server {
+ buffer_servers.push(ServerData::WithBinaryStatus(server_name, status));
+ } else {
+ other_servers.push(ServerData::WithBinaryStatus(server_name, status));
+ }
+ }
+
+ buffer_servers.sort_by_key(|data| data.name().clone());
+ other_servers.sort_by_key(|data| data.name().clone());
+ let mut other_servers_start_index = None;
+ let mut new_lsp_items =
+ Vec::with_capacity(buffer_servers.len() + other_servers.len() + 2);
+ if !buffer_servers.is_empty() {
+ new_lsp_items.push(LspItem::Header(SharedString::new("Current Buffer")));
+ new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
+ }
+ if !other_servers.is_empty() {
+ other_servers_start_index = Some(new_lsp_items.len());
+ new_lsp_items.push(LspItem::Header(SharedString::new("Other Active Servers")));
+ new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
+ }
+
+ self.items = new_lsp_items;
+ self.other_servers_start_index = other_servers_start_index;
+ });
+ }
+}
+
+impl LanguageServers {
+ fn update_binary_status(
+ &mut self,
+ binary_status: BinaryStatus,
+ message: Option<&str>,
+ name: LanguageServerName,
+ ) {
+ let binary_status_message = message.map(SharedString::new);
+ if matches!(
+ binary_status,
+ BinaryStatus::Stopped | BinaryStatus::Failed { .. }
+ ) {
+ self.health_statuses.retain(|_, server| server.name != name);
+ }
+ self.binary_statuses.insert(
+ name,
+ LanguageServerBinaryStatus {
+ status: binary_status,
+ message: binary_status_message,
+ },
+ );
+ }
+
+ fn update_server_health(
+ &mut self,
+ id: LanguageServerId,
+ health: ServerHealth,
+ message: Option<&str>,
+ name: Option<LanguageServerName>,
+ ) {
+ if let Some(state) = self.health_statuses.get_mut(&id) {
+ state.health = Some((message.map(SharedString::new), health));
+ if let Some(name) = name {
+ state.name = name;
+ }
+ } else if let Some(name) = name {
+ self.health_statuses.insert(
+ id,
+ LanguageServerHealthStatus {
+ health: Some((message.map(SharedString::new), health)),
+ name,
+ },
+ );
+ }
+ }
+}
+
+#[derive(Debug)]
+enum ServerData<'a> {
+ WithHealthCheck(
+ LanguageServerId,
+ &'a LanguageServerHealthStatus,
+ Option<&'a LanguageServerBinaryStatus>,
+ ),
+ WithBinaryStatus(&'a LanguageServerName, &'a LanguageServerBinaryStatus),
+}
+
+#[derive(Debug)]
+enum LspItem {
+ WithHealthCheck(
+ LanguageServerId,
+ LanguageServerHealthStatus,
+ Option<LanguageServerBinaryStatus>,
+ ),
+ WithBinaryStatus(LanguageServerName, LanguageServerBinaryStatus),
+ Header(SharedString),
+}
+
+impl ServerData<'_> {
+ fn name(&self) -> &LanguageServerName {
+ match self {
+ Self::WithHealthCheck(_, state, _) => &state.name,
+ Self::WithBinaryStatus(name, ..) => name,
+ }
+ }
+
+ fn into_lsp_item(self) -> LspItem {
+ match self {
+ Self::WithHealthCheck(id, name, status) => {
+ LspItem::WithHealthCheck(id, name.clone(), status.cloned())
+ }
+ Self::WithBinaryStatus(name, status) => {
+ LspItem::WithBinaryStatus(name.clone(), status.clone())
+ }
+ }
+ }
+}
+
+impl PickerDelegate for LspPickerDelegate {
+ type ListItem = AnyElement;
+
+ fn match_count(&self) -> usize {
+ self.items.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ self.selected_index = ix;
+ cx.notify();
+ }
+
+ fn update_matches(
+ &mut self,
+ _: String,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ cx.spawn(async move |lsp_picker, cx| {
+ cx.background_executor()
+ .timer(Duration::from_millis(30))
+ .await;
+ lsp_picker
+ .update(cx, |lsp_picker, cx| {
+ lsp_picker.delegate.regenerate_items(cx);
+ })
+ .ok();
+ })
+ }
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ Arc::default()
+ }
+
+ fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context<Picker<Self>>) {}
+
+ fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ cx.emit(DismissEvent);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ _: bool,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let is_other_server = self
+ .other_servers_start_index
+ .map_or(false, |start| ix >= start);
+ let server_binary_status;
+ let server_health;
+ let server_message;
+ let server_id;
+ let server_name;
+ match self.items.get(ix)? {
+ LspItem::WithHealthCheck(
+ language_server_id,
+ language_server_health_status,
+ language_server_binary_status,
+ ) => {
+ server_binary_status = language_server_binary_status.as_ref();
+ server_health = language_server_health_status.health();
+ server_message = language_server_health_status.message();
+ server_id = Some(*language_server_id);
+ server_name = language_server_health_status.name.clone();
+ }
+ LspItem::WithBinaryStatus(language_server_name, language_server_binary_status) => {
+ server_binary_status = Some(language_server_binary_status);
+ server_health = None;
+ server_message = language_server_binary_status.message.clone();
+ server_id = None;
+ server_name = language_server_name.clone();
+ }
+ LspItem::Header(header) => {
+ return Some(
+ h_flex()
+ .justify_center()
+ .child(Label::new(header.clone()))
+ .into_any_element(),
+ );
+ }
+ };
+
+ let workspace = self.state.read(cx).workspace.clone();
+ let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
+ let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
+ let server_selector = server_id
+ .map(LanguageServerSelector::Id)
+ .unwrap_or_else(|| LanguageServerSelector::Name(server_name.clone()));
+ let can_stop = server_binary_status.is_none_or(|status| {
+ matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
+ });
+ // TODO currently, Zed remote does not work well with the LSP logs
+ // https://github.com/zed-industries/zed/issues/28557
+ let has_logs = lsp_store.read(cx).as_local().is_some()
+ && lsp_logs.read(cx).has_server_logs(&server_selector);
+ let status_color = server_binary_status
+ .and_then(|binary_status| match binary_status.status {
+ BinaryStatus::None => None,
+ BinaryStatus::CheckingForUpdate
+ | BinaryStatus::Downloading
+ | BinaryStatus::Starting => Some(Color::Modified),
+ BinaryStatus::Stopping => Some(Color::Disabled),
+ BinaryStatus::Stopped => Some(Color::Disabled),
+ BinaryStatus::Failed { .. } => Some(Color::Error),
+ })
+ .or_else(|| {
+ Some(match server_health? {
+ ServerHealth::Ok => Color::Success,
+ ServerHealth::Warning => Color::Warning,
+ ServerHealth::Error => Color::Error,
+ })
+ })
+ .unwrap_or(Color::Success);
+
+ Some(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .gap_2()
+ .child(
+ h_flex()
+ .id("server-status-indicator")
+ .gap_2()
+ .child(Indicator::dot().color(status_color))
+ .child(Label::new(server_name.0.clone()))
+ .when_some(server_message.clone(), |div, server_message| {
+ div.tooltip(move |_, cx| Tooltip::simple(server_message.clone(), cx))
+ }),
+ )
+ .child(
+ h_flex()
+ .gap_1()
+ .when(has_logs, |div| {
+ div.child(
+ IconButton::new("debug-language-server", IconName::MessageBubbles)
+ .icon_size(IconSize::XSmall)
+ .tooltip(|_, cx| Tooltip::simple("Debug Language Server", cx))
+ .on_click({
+ let workspace = workspace.clone();
+ let lsp_logs = lsp_logs.downgrade();
+ let server_selector = server_selector.clone();
+ move |_, window, cx| {
+ lsp_logs
+ .update(cx, |lsp_logs, cx| {
+ lsp_logs.open_server_trace(
+ workspace.clone(),
+ server_selector.clone(),
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
+ }),
+ )
+ })
+ .when(can_stop, |div| {
+ div.child(
+ IconButton::new("stop-server", IconName::Stop)
+ .icon_size(IconSize::Small)
+ .tooltip(|_, cx| Tooltip::simple("Stop server", cx))
+ .on_click({
+ let lsp_store = lsp_store.downgrade();
+ let server_selector = server_selector.clone();
+ move |_, _, cx| {
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.stop_language_servers_for_buffers(
+ Vec::new(),
+ HashSet::from_iter([
+ server_selector.clone()
+ ]),
+ cx,
+ );
+ })
+ .ok();
+ }
+ }),
+ )
+ })
+ .child(
+ IconButton::new("restart-server", IconName::Rerun)
+ .icon_size(IconSize::XSmall)
+ .tooltip(|_, cx| Tooltip::simple("Restart server", cx))
+ .on_click({
+ let state = self.state.clone();
+ let workspace = workspace.clone();
+ let lsp_store = lsp_store.downgrade();
+ let editor_buffers = state
+ .read(cx)
+ .active_editor
+ .as_ref()
+ .map(|active_editor| active_editor.editor_buffers.clone())
+ .unwrap_or_default();
+ let server_selector = server_selector.clone();
+ move |_, _, cx| {
+ if let Some(workspace) = workspace.upgrade() {
+ let project = workspace.read(cx).project().clone();
+ let buffer_store =
+ project.read(cx).buffer_store().clone();
+ let buffers = if is_other_server {
+ let worktree_store =
+ project.read(cx).worktree_store();
+ state
+ .read(cx)
+ .language_servers
+ .servers_per_buffer_abs_path
+ .iter()
+ .filter_map(|(abs_path, servers)| {
+ if servers.values().any(|server| {
+ server.as_ref() == Some(&server_name)
+ }) {
+ worktree_store
+ .read(cx)
+ .find_worktree(abs_path, cx)
+ } else {
+ None
+ }
+ })
+ .filter_map(|(worktree, relative_path)| {
+ let entry = worktree
+ .read(cx)
+ .entry_for_path(&relative_path)?;
+ project
+ .read(cx)
+ .path_for_entry(entry.id, cx)
+ })
+ .filter_map(|project_path| {
+ buffer_store
+ .read(cx)
+ .get_by_path(&project_path)
+ })
+ .collect::<Vec<_>>()
+ } else {
+ editor_buffers
+ .iter()
+ .flat_map(|buffer_id| {
+ buffer_store.read(cx).get(*buffer_id)
+ })
+ .collect::<Vec<_>>()
+ };
+ if !buffers.is_empty() {
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store
+ .restart_language_servers_for_buffers(
+ buffers,
+ HashSet::from_iter([
+ server_selector.clone(),
+ ]),
+ cx,
+ );
+ })
+ .ok();
+ }
+ }
+ }
+ }),
+ ),
+ )
+ .cursor_default()
+ .into_any_element(),
+ )
+ }
+
+ fn render_editor(
+ &self,
+ editor: &Entity<Editor>,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Div {
+ div().child(div().track_focus(&editor.focus_handle(cx)))
+ }
+
+ fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
+ if self.items.is_empty() {
+ Some(
+ h_flex()
+ .w_full()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Button::new("stop-all-servers", "Stop all servers")
+ .disabled(true)
+ .on_click(move |_, _, _| {})
+ .full_width(),
+ )
+ .into_any_element(),
+ )
+ } else {
+ let lsp_store = self.state.read(cx).lsp_store.clone();
+ Some(
+ h_flex()
+ .w_full()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Button::new("stop-all-servers", "Stop all servers")
+ .on_click({
+ move |_, _, cx| {
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.stop_all_language_servers(cx);
+ })
+ .ok();
+ }
+ })
+ .full_width(),
+ )
+ .into_any_element(),
+ )
+ }
+ }
+
+ fn separators_after_indices(&self) -> Vec<usize> {
+ if self.items.is_empty() {
+ Vec::new()
+ } else {
+ vec![self.items.len() - 1]
+ }
+ }
+}
+
+// TODO kb keyboard story
+impl LspTool {
+ pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ let settings_subscription =
+ cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
+ if ProjectSettings::get_global(cx).global_lsp_settings.button {
+ if lsp_tool.lsp_picker.is_none() {
+ lsp_tool.lsp_picker =
+ Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx));
+ cx.notify();
+ return;
+ }
+ } else if lsp_tool.lsp_picker.take().is_some() {
+ cx.notify();
+ }
+ });
+
+ let lsp_store = workspace.project().read(cx).lsp_store();
+ let lsp_store_subscription =
+ cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
+ lsp_tool.on_lsp_store_event(e, window, cx)
+ });
+
+ let state = cx.new(|_| PickerState {
+ workspace: workspace.weak_handle(),
+ lsp_store: lsp_store.downgrade(),
+ active_editor: None,
+ language_servers: LanguageServers::default(),
+ });
+
+ Self {
+ state,
+ lsp_picker: None,
+ _subscriptions: vec![settings_subscription, lsp_store_subscription],
+ }
+ }
+
+ fn on_lsp_store_event(
+ &mut self,
+ e: &LspStoreEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(lsp_picker) = self.lsp_picker.clone() else {
+ return;
+ };
+ let mut updated = false;
+
+ match e {
+ LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ name,
+ message: proto::update_language_server::Variant::StatusUpdate(status_update),
+ } => match &status_update.status {
+ Some(proto::status_update::Status::Binary(binary_status)) => {
+ let Some(name) = name.as_ref() else {
+ return;
+ };
+ if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
+ {
+ let binary_status = match binary_status {
+ proto::ServerBinaryStatus::None => BinaryStatus::None,
+ proto::ServerBinaryStatus::CheckingForUpdate => {
+ BinaryStatus::CheckingForUpdate
+ }
+ proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
+ proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
+ proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
+ proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
+ proto::ServerBinaryStatus::Failed => {
+ let Some(error) = status_update.message.clone() else {
+ return;
+ };
+ BinaryStatus::Failed { error }
+ }
+ };
+ self.state.update(cx, |state, _| {
+ state.language_servers.update_binary_status(
+ binary_status,
+ status_update.message.as_deref(),
+ name.clone(),
+ );
+ });
+ updated = true;
+ };
+ }
+ Some(proto::status_update::Status::Health(health_status)) => {
+ if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
+ let health = match health {
+ proto::ServerHealth::Ok => ServerHealth::Ok,
+ proto::ServerHealth::Warning => ServerHealth::Warning,
+ proto::ServerHealth::Error => ServerHealth::Error,
+ };
+ self.state.update(cx, |state, _| {
+ state.language_servers.update_server_health(
+ *language_server_id,
+ health,
+ status_update.message.as_deref(),
+ name.clone(),
+ );
+ });
+ updated = true;
+ }
+ }
+ None => {}
+ },
+ LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ name,
+ message: proto::update_language_server::Variant::RegisteredForBuffer(update),
+ ..
+ } => {
+ self.state.update(cx, |state, _| {
+ state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .entry(PathBuf::from(&update.buffer_abs_path))
+ .or_default()
+ .insert(*language_server_id, name.clone());
+ });
+ updated = true;
+ }
+ _ => {}
+ };
+
+ if updated {
+ lsp_picker.update(cx, |lsp_picker, cx| {
+ lsp_picker.refresh(window, cx);
+ });
+ }
+ }
+
+ fn new_lsp_picker(
+ state: Entity<PickerState>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Entity<Picker<LspPickerDelegate>> {
+ cx.new(|cx| {
+ let mut delegate = LspPickerDelegate {
+ selected_index: 0,
+ other_servers_start_index: None,
+ items: Vec::new(),
+ state,
+ };
+ delegate.regenerate_items(cx);
+ Picker::list(delegate, window, cx)
+ })
+ }
+}
+
+impl StatusItemView for LspTool {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn workspace::ItemHandle>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if ProjectSettings::get_global(cx).global_lsp_settings.button {
+ if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+ if Some(&editor)
+ != self
+ .state
+ .read(cx)
+ .active_editor
+ .as_ref()
+ .and_then(|active_editor| active_editor.editor.upgrade())
+ .as_ref()
+ {
+ let editor_buffers =
+ HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
+ let _editor_subscription = cx.subscribe_in(
+ &editor,
+ window,
+ |lsp_tool, _, e: &EditorEvent, window, cx| match e {
+ EditorEvent::ExcerptsAdded { buffer, .. } => {
+ lsp_tool.state.update(cx, |state, cx| {
+ if let Some(active_editor) = state.active_editor.as_mut() {
+ let buffer_id = buffer.read(cx).remote_id();
+ if active_editor.editor_buffers.insert(buffer_id) {
+ if let Some(picker) = &lsp_tool.lsp_picker {
+ picker.update(cx, |picker, cx| {
+ picker.refresh(window, cx)
+ });
+ }
+ }
+ }
+ });
+ }
+ EditorEvent::ExcerptsRemoved {
+ removed_buffer_ids, ..
+ } => {
+ lsp_tool.state.update(cx, |state, cx| {
+ if let Some(active_editor) = state.active_editor.as_mut() {
+ let mut removed = false;
+ for id in removed_buffer_ids {
+ active_editor.editor_buffers.retain(|buffer_id| {
+ let retain = buffer_id != id;
+ removed |= !retain;
+ retain
+ });
+ }
+ if removed {
+ if let Some(picker) = &lsp_tool.lsp_picker {
+ picker.update(cx, |picker, cx| {
+ picker.refresh(window, cx)
+ });
+ }
+ }
+ }
+ });
+ }
+ _ => {}
+ },
+ );
+ self.state.update(cx, |state, _| {
+ state.active_editor = Some(ActiveEditor {
+ editor: editor.downgrade(),
+ _editor_subscription,
+ editor_buffers,
+ });
+ });
+
+ let lsp_picker = Self::new_lsp_picker(self.state.clone(), window, cx);
+ self.lsp_picker = Some(lsp_picker.clone());
+ lsp_picker.update(cx, |lsp_picker, cx| lsp_picker.refresh(window, cx));
+ }
+ } else if self.state.read(cx).active_editor.is_some() {
+ self.state.update(cx, |state, _| {
+ state.active_editor = None;
+ });
+ if let Some(lsp_picker) = self.lsp_picker.as_ref() {
+ lsp_picker.update(cx, |lsp_picker, cx| {
+ lsp_picker.refresh(window, cx);
+ });
+ };
+ }
+ } else if self.state.read(cx).active_editor.is_some() {
+ self.state.update(cx, |state, _| {
+ state.active_editor = None;
+ });
+ if let Some(lsp_picker) = self.lsp_picker.as_ref() {
+ lsp_picker.update(cx, |lsp_picker, cx| {
+ lsp_picker.refresh(window, cx);
+ });
+ }
+ }
+ }
+}
+
+impl Render for LspTool {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+ let Some(lsp_picker) = self.lsp_picker.clone() else {
+ return div();
+ };
+
+ let mut has_errors = false;
+ let mut has_warnings = false;
+ let mut has_other_notifications = false;
+ let state = self.state.read(cx);
+ for server in state.language_servers.health_statuses.values() {
+ if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
+ has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
+ has_other_notifications |= binary_status.message.is_some();
+ }
+
+ if let Some((message, health)) = &server.health {
+ has_other_notifications |= message.is_some();
+ match health {
+ ServerHealth::Ok => {}
+ ServerHealth::Warning => has_warnings = true,
+ ServerHealth::Error => has_errors = true,
+ }
+ }
+ }
+
+ let indicator = if has_errors {
+ Some(Indicator::dot().color(Color::Error))
+ } else if has_warnings {
+ Some(Indicator::dot().color(Color::Warning))
+ } else if has_other_notifications {
+ Some(Indicator::dot().color(Color::Modified))
+ } else {
+ None
+ };
+
+ div().child(
+ PickerPopoverMenu::new(
+ lsp_picker.clone(),
+ IconButton::new("zed-lsp-tool-button", IconName::Bolt)
+ .when_some(indicator, IconButton::indicator)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::XSmall)
+ .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
+ move |_, cx| Tooltip::simple("Language servers", cx),
+ Corner::BottomRight,
+ cx,
+ )
+ .render(window, cx),
+ )
+ }
+}
@@ -108,6 +108,12 @@ pub struct LanguageServer {
root_uri: Url,
}
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum LanguageServerSelector {
+ Id(LanguageServerId),
+ Name(LanguageServerName),
+}
+
/// Identifies a running language server.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
@@ -205,6 +205,7 @@ pub trait PickerDelegate: Sized + 'static {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem>;
+
fn render_header(
&self,
_window: &mut Window,
@@ -212,6 +213,7 @@ pub trait PickerDelegate: Sized + 'static {
) -> Option<AnyElement> {
None
}
+
fn render_footer(
&self,
_window: &mut Window,
@@ -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
@@ -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
@@ -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)?;
@@ -42,9 +42,8 @@ use itertools::Itertools as _;
use language::{
Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
- LanguageRegistry, LanguageServerStatusUpdate, LanguageToolchainStore, LocalFile, LspAdapter,
- LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
- Unclipped,
+ LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
+ PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
},
@@ -60,9 +59,9 @@ use lsp::{
DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind,
FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer,
LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName,
- LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
- TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
- notification::DidRenameFiles,
+ LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf,
+ RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams,
+ WorkspaceFolder, notification::DidRenameFiles,
};
use node_runtime::read_package_installed_version;
use parking_lot::Mutex;
@@ -256,7 +255,7 @@ impl LocalLspStore {
let delegate = delegate as Arc<dyn LspAdapterDelegate>;
let key = key.clone();
let adapter = adapter.clone();
- let this = self.weak.clone();
+ let lsp_store = self.weak.clone();
let pending_workspace_folders = pending_workspace_folders.clone();
let fs = self.fs.clone();
let pull_diagnostics = ProjectSettings::get_global(cx)
@@ -265,7 +264,8 @@ impl LocalLspStore {
.enabled;
cx.spawn(async move |cx| {
let result = async {
- let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?;
+ let toolchains =
+ lsp_store.update(cx, |lsp_store, cx| lsp_store.toolchain_store(cx))?;
let language_server = pending_server.await?;
let workspace_config = Self::workspace_configuration_for_adapter(
@@ -300,7 +300,7 @@ impl LocalLspStore {
})??;
Self::setup_lsp_messages(
- this.clone(),
+ lsp_store.clone(),
fs,
&language_server,
delegate.clone(),
@@ -321,7 +321,7 @@ impl LocalLspStore {
})?
.await
.inspect_err(|_| {
- if let Some(lsp_store) = this.upgrade() {
+ if let Some(lsp_store) = lsp_store.upgrade() {
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.cleanup_lsp_data(server_id);
@@ -343,17 +343,18 @@ impl LocalLspStore {
match result {
Ok(server) => {
- this.update(cx, |this, mut cx| {
- this.insert_newly_running_language_server(
- adapter,
- server.clone(),
- server_id,
- key,
- pending_workspace_folders,
- &mut cx,
- );
- })
- .ok();
+ lsp_store
+ .update(cx, |lsp_store, mut cx| {
+ lsp_store.insert_newly_running_language_server(
+ adapter,
+ server.clone(),
+ server_id,
+ key,
+ pending_workspace_folders,
+ &mut cx,
+ );
+ })
+ .ok();
stderr_capture.lock().take();
Some(server)
}
@@ -366,7 +367,9 @@ impl LocalLspStore {
error: format!("{err}\n-- stderr--\n{log}"),
},
);
- log::error!("Failed to start language server {server_name:?}: {err:#?}");
+ let message =
+ format!("Failed to start language server {server_name:?}: {err:#?}");
+ log::error!("{message}");
log::error!("server stderr: {log}");
None
}
@@ -378,6 +381,9 @@ impl LocalLspStore {
pending_workspace_folders,
};
+ self.languages
+ .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting);
+
self.language_servers.insert(server_id, state);
self.language_server_ids
.entry(key)
@@ -1028,20 +1034,14 @@ impl LocalLspStore {
clangd_ext::register_notifications(this, language_server, adapter);
}
- fn shutdown_language_servers(
+ fn shutdown_language_servers_on_quit(
&mut self,
- _cx: &mut Context<LspStore>,
+ _: &mut Context<LspStore>,
) -> impl Future<Output = ()> + use<> {
let shutdown_futures = self
.language_servers
.drain()
- .map(|(_, server_state)| async {
- use LanguageServerState::*;
- match server_state {
- Running { server, .. } => server.shutdown()?.await,
- Starting { startup, .. } => startup.await?.shutdown()?.await,
- }
- })
+ .map(|(_, server_state)| Self::shutdown_server(server_state))
.collect::<Vec<_>>();
async move {
@@ -1049,6 +1049,24 @@ impl LocalLspStore {
}
}
+ async fn shutdown_server(server_state: LanguageServerState) -> anyhow::Result<()> {
+ match server_state {
+ LanguageServerState::Running { server, .. } => {
+ if let Some(shutdown) = server.shutdown() {
+ shutdown.await;
+ }
+ }
+ LanguageServerState::Starting { startup, .. } => {
+ if let Some(server) = startup.await {
+ if let Some(shutdown) = server.shutdown() {
+ shutdown.await;
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
fn language_servers_for_worktree(
&self,
worktree_id: WorktreeId,
@@ -2318,6 +2336,7 @@ impl LocalLspStore {
fn register_buffer_with_language_servers(
&mut self,
buffer_handle: &Entity<Buffer>,
+ only_register_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<LspStore>,
) {
let buffer = buffer_handle.read(cx);
@@ -2383,6 +2402,18 @@ impl LocalLspStore {
if reused && server_node.server_id().is_none() {
return None;
}
+ if !only_register_servers.is_empty() {
+ if let Some(server_id) = server_node.server_id() {
+ if !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) {
+ return None;
+ }
+ }
+ if let Some(name) = server_node.name() {
+ if !only_register_servers.contains(&LanguageServerSelector::Name(name)) {
+ return None;
+ }
+ }
+ }
let server_id = server_node.server_id_or_init(
|LaunchDisposition {
@@ -2390,66 +2421,82 @@ impl LocalLspStore {
attach,
path,
settings,
- }| match attach {
- language::Attach::InstancePerRoot => {
- // todo: handle instance per root proper.
- if let Some(server_ids) = self
- .language_server_ids
- .get(&(worktree_id, server_name.clone()))
- {
- server_ids.iter().cloned().next().unwrap()
- } else {
- let language_name = language.name();
-
- self.start_language_server(
- &worktree,
- delegate.clone(),
- self.languages
- .lsp_adapters(&language_name)
- .into_iter()
- .find(|adapter| &adapter.name() == server_name)
- .expect("To find LSP adapter"),
- settings,
- cx,
- )
- }
- }
- language::Attach::Shared => {
- let uri = Url::from_file_path(
- worktree.read(cx).abs_path().join(&path.path),
- );
- let key = (worktree_id, server_name.clone());
- if !self.language_server_ids.contains_key(&key) {
- let language_name = language.name();
- self.start_language_server(
- &worktree,
- delegate.clone(),
- self.languages
- .lsp_adapters(&language_name)
- .into_iter()
- .find(|adapter| &adapter.name() == server_name)
- .expect("To find LSP adapter"),
- settings,
- cx,
- );
- }
- if let Some(server_ids) = self
- .language_server_ids
- .get(&key)
- {
- debug_assert_eq!(server_ids.len(), 1);
- let server_id = server_ids.iter().cloned().next().unwrap();
-
- if let Some(state) = self.language_servers.get(&server_id) {
- if let Ok(uri) = uri {
- state.add_workspace_folder(uri);
- };
- }
- server_id
- } else {
- unreachable!("Language server ID should be available, as it's registered on demand")
- }
- }
+ }| {
+ let server_id = match attach {
+ language::Attach::InstancePerRoot => {
+ // todo: handle instance per root proper.
+ if let Some(server_ids) = self
+ .language_server_ids
+ .get(&(worktree_id, server_name.clone()))
+ {
+ server_ids.iter().cloned().next().unwrap()
+ } else {
+ let language_name = language.name();
+ let adapter = self.languages
+ .lsp_adapters(&language_name)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter");
+ let server_id = self.start_language_server(
+ &worktree,
+ delegate.clone(),
+ adapter,
+ settings,
+ cx,
+ );
+ server_id
+ }
+ }
+ language::Attach::Shared => {
+ let uri = Url::from_file_path(
+ worktree.read(cx).abs_path().join(&path.path),
+ );
+ let key = (worktree_id, server_name.clone());
+ if !self.language_server_ids.contains_key(&key) {
+ let language_name = language.name();
+ let adapter = self.languages
+ .lsp_adapters(&language_name)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter");
+ self.start_language_server(
+ &worktree,
+ delegate.clone(),
+ adapter,
+ settings,
+ cx,
+ );
+ }
+ if let Some(server_ids) = self
+ .language_server_ids
+ .get(&key)
+ {
+ debug_assert_eq!(server_ids.len(), 1);
+ let server_id = server_ids.iter().cloned().next().unwrap();
+ if let Some(state) = self.language_servers.get(&server_id) {
+ if let Ok(uri) = uri {
+ state.add_workspace_folder(uri);
+ };
+ }
+ server_id
+ } else {
+ unreachable!("Language server ID should be available, as it's registered on demand")
+ }
+ }
+ };
+ let lsp_tool = self.weak.clone();
+ let server_name = server_node.name();
+ let buffer_abs_path = abs_path.to_string_lossy().to_string();
+ cx.defer(move |cx| {
+ lsp_tool.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id: server_id,
+ name: server_name,
+ message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer {
+ buffer_abs_path,
+ })
+ })).ok();
+ });
+ server_id
},
)?;
let server_state = self.language_servers.get(&server_id)?;
@@ -2498,6 +2545,16 @@ impl LocalLspStore {
vec![snapshot]
});
+
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id: server.server_id(),
+ name: None,
+ message: proto::update_language_server::Variant::RegisteredForBuffer(
+ proto::RegisteredForBuffer {
+ buffer_abs_path: abs_path.to_string_lossy().to_string(),
+ },
+ ),
+ });
}
}
@@ -3479,7 +3536,7 @@ pub struct LspStore {
worktree_store: Entity<WorktreeStore>,
toolchain_store: Option<Entity<ToolchainStore>>,
pub languages: Arc<LanguageRegistry>,
- pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
+ language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
active_entry: Option<ProjectEntryId>,
_maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
_maintain_buffer_languages: Task<()>,
@@ -3503,11 +3560,13 @@ struct BufferLspData {
colors: Option<Vec<DocumentColor>>,
}
+#[derive(Debug)]
pub enum LspStoreEvent {
LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
LanguageServerRemoved(LanguageServerId),
LanguageServerUpdate {
language_server_id: LanguageServerId,
+ name: Option<LanguageServerName>,
message: proto::update_language_server::Variant,
},
LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
@@ -3682,6 +3741,7 @@ impl LspStore {
}
cx.observe_global::<SettingsStore>(Self::on_settings_changed)
.detach();
+ subscribe_to_binary_statuses(&languages, cx).detach();
let _maintain_workspace_config = {
let (sender, receiver) = watch::channel();
@@ -3714,7 +3774,9 @@ impl LspStore {
next_diagnostic_group_id: Default::default(),
diagnostics: Default::default(),
_subscription: cx.on_app_quit(|this, cx| {
- this.as_local_mut().unwrap().shutdown_language_servers(cx)
+ this.as_local_mut()
+ .unwrap()
+ .shutdown_language_servers_on_quit(cx)
}),
lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx),
registered_buffers: HashMap::default(),
@@ -3768,6 +3830,7 @@ impl LspStore {
.detach();
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
+ subscribe_to_binary_statuses(&languages, cx).detach();
let _maintain_workspace_config = {
let (sender, receiver) = watch::channel();
(Self::maintain_workspace_config(fs, receiver, cx), sender)
@@ -3819,7 +3882,7 @@ 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, cx);
+ local.register_buffer_with_language_servers(buffer, HashSet::default(), cx);
}
}
}
@@ -4047,6 +4110,7 @@ impl LspStore {
pub(crate) fn register_buffer_with_language_servers(
&mut self,
buffer: &Entity<Buffer>,
+ only_register_servers: HashSet<LanguageServerSelector>,
ignore_refcounts: bool,
cx: &mut Context<Self>,
) -> OpenLspBufferHandle {
@@ -4070,7 +4134,7 @@ impl LspStore {
}
if ignore_refcounts || *refcount == 1 {
- local.register_buffer_with_language_servers(buffer, cx);
+ local.register_buffer_with_language_servers(buffer, only_register_servers, cx);
}
if !ignore_refcounts {
cx.observe_release(&handle, move |this, buffer, cx| {
@@ -4097,6 +4161,26 @@ impl LspStore {
.request(proto::RegisterBufferWithLanguageServers {
project_id: upstream_project_id,
buffer_id,
+ only_servers: only_register_servers
+ .into_iter()
+ .map(|selector| {
+ let selector = match selector {
+ LanguageServerSelector::Id(language_server_id) => {
+ proto::language_server_selector::Selector::ServerId(
+ language_server_id.to_proto(),
+ )
+ }
+ LanguageServerSelector::Name(language_server_name) => {
+ proto::language_server_selector::Selector::Name(
+ language_server_name.to_string(),
+ )
+ }
+ };
+ proto::LanguageServerSelector {
+ selector: Some(selector),
+ }
+ })
+ .collect(),
})
.await
})
@@ -4182,7 +4266,11 @@ impl LspStore {
.registered_buffers
.contains_key(&buffer.read(cx).remote_id())
{
- local.register_buffer_with_language_servers(&buffer, cx);
+ local.register_buffer_with_language_servers(
+ &buffer,
+ HashSet::default(),
+ cx,
+ );
}
}
}
@@ -4267,7 +4355,11 @@ impl LspStore {
if let Some(local) = self.as_local_mut() {
if local.registered_buffers.contains_key(&buffer_id) {
- local.register_buffer_with_language_servers(buffer_entity, cx);
+ local.register_buffer_with_language_servers(
+ buffer_entity,
+ HashSet::default(),
+ cx,
+ );
}
}
Some(worktree.read(cx).id())
@@ -4488,28 +4580,29 @@ impl LspStore {
let buffer_store = self.buffer_store.clone();
if let Some(local) = self.as_local_mut() {
let mut adapters = BTreeMap::default();
- let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| {
- let get_adapter = {
- let languages = local.languages.clone();
- let environment = local.environment.clone();
- let weak = local.weak.clone();
- let worktree_store = local.worktree_store.clone();
- let http_client = local.http_client.clone();
- let fs = local.fs.clone();
- move |worktree_id, cx: &mut App| {
- let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
- Some(LocalLspAdapterDelegate::new(
- languages.clone(),
- &environment,
- weak.clone(),
- &worktree,
- http_client.clone(),
- fs.clone(),
- cx,
- ))
- }
- };
+ let get_adapter = {
+ let languages = local.languages.clone();
+ let environment = local.environment.clone();
+ let weak = local.weak.clone();
+ let worktree_store = local.worktree_store.clone();
+ let http_client = local.http_client.clone();
+ let fs = local.fs.clone();
+ move |worktree_id, cx: &mut App| {
+ let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
+ Some(LocalLspAdapterDelegate::new(
+ languages.clone(),
+ &environment,
+ weak.clone(),
+ &worktree,
+ http_client.clone(),
+ fs.clone(),
+ cx,
+ ))
+ }
+ };
+ let mut messages_to_report = Vec::new();
+ let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| {
let mut rebase = lsp_tree.rebase();
for buffer_handle in buffer_store.read(cx).buffers().sorted_by_key(|buffer| {
Reverse(
@@ -4570,9 +4663,10 @@ impl LspStore {
continue;
};
+ let abs_path = file.abs_path(cx);
for node in nodes {
if !reused {
- node.server_id_or_init(
+ let server_id = node.server_id_or_init(
|LaunchDisposition {
server_name,
attach,
@@ -4587,20 +4681,20 @@ impl LspStore {
{
server_ids.iter().cloned().next().unwrap()
} else {
- local.start_language_server(
+ let adapter = local
+ .languages
+ .lsp_adapters(&language)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter");
+ let server_id = local.start_language_server(
&worktree,
delegate.clone(),
- local
- .languages
- .lsp_adapters(&language)
- .into_iter()
- .find(|adapter| {
- &adapter.name() == server_name
- })
- .expect("To find LSP adapter"),
+ adapter,
settings,
cx,
- )
+ );
+ server_id
}
}
language::Attach::Shared => {
@@ -4610,15 +4704,16 @@ impl LspStore {
let key = (worktree_id, server_name.clone());
local.language_server_ids.remove(&key);
+ let adapter = local
+ .languages
+ .lsp_adapters(&language)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter");
let server_id = local.start_language_server(
&worktree,
delegate.clone(),
- local
- .languages
- .lsp_adapters(&language)
- .into_iter()
- .find(|adapter| &adapter.name() == server_name)
- .expect("To find LSP adapter"),
+ adapter,
settings,
cx,
);
@@ -4633,14 +4728,30 @@ impl LspStore {
}
},
);
+
+ if let Some(language_server_id) = server_id {
+ messages_to_report.push(LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ name: node.name(),
+ message:
+ proto::update_language_server::Variant::RegisteredForBuffer(
+ proto::RegisteredForBuffer {
+ buffer_abs_path: abs_path.to_string_lossy().to_string(),
+ },
+ ),
+ });
+ }
}
}
}
}
rebase.finish()
});
- for (id, name) in to_stop {
- self.stop_local_language_server(id, name, cx).detach();
+ for message in messages_to_report {
+ cx.emit(message);
+ }
+ for (id, _) in to_stop {
+ self.stop_local_language_server(id, cx).detach();
}
}
}
@@ -7122,7 +7233,7 @@ impl LspStore {
path: relative_path.into(),
};
- if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
+ if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) {
let snapshot = buffer_handle.read(cx).snapshot();
let buffer = buffer_handle.read(cx);
let reused_diagnostics = buffer
@@ -7801,6 +7912,7 @@ impl LspStore {
return upstream_client.send(proto::RegisterBufferWithLanguageServers {
project_id: upstream_project_id,
buffer_id: buffer_id.to_proto(),
+ only_servers: envelope.payload.only_servers,
});
}
@@ -7808,7 +7920,28 @@ impl LspStore {
anyhow::bail!("buffer is not open");
};
- let handle = this.register_buffer_with_language_servers(&buffer, false, cx);
+ let handle = this.register_buffer_with_language_servers(
+ &buffer,
+ envelope
+ .payload
+ .only_servers
+ .into_iter()
+ .filter_map(|selector| {
+ Some(match selector.selector? {
+ proto::language_server_selector::Selector::ServerId(server_id) => {
+ LanguageServerSelector::Id(LanguageServerId::from_proto(server_id))
+ }
+ proto::language_server_selector::Selector::Name(name) => {
+ LanguageServerSelector::Name(LanguageServerName(
+ SharedString::from(name),
+ ))
+ }
+ })
+ })
+ .collect(),
+ false,
+ cx,
+ );
this.buffer_store().update(cx, |buffer_store, _| {
buffer_store.register_shared_lsp_handle(peer_id, buffer_id, handle);
});
@@ -7980,16 +8113,16 @@ impl LspStore {
}
async fn handle_update_language_server(
- this: Entity<Self>,
+ lsp_store: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateLanguageServer>,
mut cx: AsyncApp,
) -> Result<()> {
- this.update(&mut cx, |this, cx| {
+ lsp_store.update(&mut cx, |lsp_store, cx| {
let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize);
match envelope.payload.variant.context("invalid variant")? {
proto::update_language_server::Variant::WorkStart(payload) => {
- this.on_lsp_work_start(
+ lsp_store.on_lsp_work_start(
language_server_id,
payload.token,
LanguageServerProgress {
@@ -8003,9 +8136,8 @@ impl LspStore {
cx,
);
}
-
proto::update_language_server::Variant::WorkProgress(payload) => {
- this.on_lsp_work_progress(
+ lsp_store.on_lsp_work_progress(
language_server_id,
payload.token,
LanguageServerProgress {
@@ -8021,15 +8153,28 @@ impl LspStore {
}
proto::update_language_server::Variant::WorkEnd(payload) => {
- this.on_lsp_work_end(language_server_id, payload.token, cx);
+ lsp_store.on_lsp_work_end(language_server_id, payload.token, cx);
}
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => {
- this.disk_based_diagnostics_started(language_server_id, cx);
+ lsp_store.disk_based_diagnostics_started(language_server_id, cx);
}
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => {
- this.disk_based_diagnostics_finished(language_server_id, cx)
+ lsp_store.disk_based_diagnostics_finished(language_server_id, cx)
+ }
+
+ non_lsp @ proto::update_language_server::Variant::StatusUpdate(_)
+ | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) => {
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id,
+ name: envelope
+ .payload
+ .server_name
+ .map(SharedString::new)
+ .map(LanguageServerName),
+ message: non_lsp,
+ });
}
}
@@ -8145,6 +8290,9 @@ impl LspStore {
cx.emit(LspStoreEvent::DiskBasedDiagnosticsStarted { language_server_id });
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name: self
+ .language_server_adapter_for_id(language_server_id)
+ .map(|adapter| adapter.name()),
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
Default::default(),
),
@@ -8165,6 +8313,9 @@ impl LspStore {
cx.emit(LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id });
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name: self
+ .language_server_adapter_for_id(language_server_id)
+ .map(|adapter| adapter.name()),
message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
Default::default(),
),
@@ -8473,6 +8624,9 @@ impl LspStore {
}
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name: self
+ .language_server_adapter_for_id(language_server_id)
+ .map(|adapter| adapter.name()),
message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
token,
title: progress.title,
@@ -8521,6 +8675,9 @@ impl LspStore {
if did_update {
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name: self
+ .language_server_adapter_for_id(language_server_id)
+ .map(|adapter| adapter.name()),
message: proto::update_language_server::Variant::WorkProgress(
proto::LspWorkProgress {
token,
@@ -8550,6 +8707,9 @@ impl LspStore {
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name: self
+ .language_server_adapter_for_id(language_server_id)
+ .map(|adapter| adapter.name()),
message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { token }),
})
}
@@ -8930,22 +9090,73 @@ impl LspStore {
envelope: TypedEnvelope<proto::RestartLanguageServers>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
- this.update(&mut cx, |this, cx| {
- let buffers = this.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx);
- this.restart_language_servers_for_buffers(buffers, cx);
+ this.update(&mut cx, |lsp_store, cx| {
+ let buffers =
+ lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx);
+ lsp_store.restart_language_servers_for_buffers(
+ buffers,
+ envelope
+ .payload
+ .only_servers
+ .into_iter()
+ .filter_map(|selector| {
+ Some(match selector.selector? {
+ proto::language_server_selector::Selector::ServerId(server_id) => {
+ LanguageServerSelector::Id(LanguageServerId::from_proto(server_id))
+ }
+ proto::language_server_selector::Selector::Name(name) => {
+ LanguageServerSelector::Name(LanguageServerName(
+ SharedString::from(name),
+ ))
+ }
+ })
+ })
+ .collect(),
+ cx,
+ );
})?;
Ok(proto::Ack {})
}
pub async fn handle_stop_language_servers(
- this: Entity<Self>,
+ lsp_store: Entity<Self>,
envelope: TypedEnvelope<proto::StopLanguageServers>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
- this.update(&mut cx, |this, cx| {
- let buffers = this.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx);
- this.stop_language_servers_for_buffers(buffers, cx);
+ lsp_store.update(&mut cx, |lsp_store, cx| {
+ if envelope.payload.all
+ && envelope.payload.also_servers.is_empty()
+ && envelope.payload.buffer_ids.is_empty()
+ {
+ lsp_store.stop_all_language_servers(cx);
+ } else {
+ let buffers =
+ lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx);
+ lsp_store.stop_language_servers_for_buffers(
+ buffers,
+ envelope
+ .payload
+ .also_servers
+ .into_iter()
+ .filter_map(|selector| {
+ Some(match selector.selector? {
+ proto::language_server_selector::Selector::ServerId(server_id) => {
+ LanguageServerSelector::Id(LanguageServerId::from_proto(
+ server_id,
+ ))
+ }
+ proto::language_server_selector::Selector::Name(name) => {
+ LanguageServerSelector::Name(LanguageServerName(
+ SharedString::from(name),
+ ))
+ }
+ })
+ })
+ .collect(),
+ cx,
+ );
+ }
})?;
Ok(proto::Ack {})
@@ -9269,11 +9480,8 @@ impl LspStore {
select! {
server = startup.fuse() => server,
- _ = timer => {
- log::info!(
- "timeout waiting for language server {} to finish launching before stopping",
- name
- );
+ () = timer => {
+ log::info!("timeout waiting for language server {name} to finish launching before stopping");
None
},
}
@@ -9296,7 +9504,6 @@ impl LspStore {
fn stop_local_language_server(
&mut self,
server_id: LanguageServerId,
- name: LanguageServerName,
cx: &mut Context<Self>,
) -> Task<Vec<WorktreeId>> {
let local = match &mut self.mode {
@@ -9306,7 +9513,7 @@ impl LspStore {
}
};
- let mut orphaned_worktrees = vec![];
+ let mut orphaned_worktrees = Vec::new();
// Remove this server ID from all entries in the given worktree.
local.language_server_ids.retain(|(worktree, _), ids| {
if !ids.remove(&server_id) {
@@ -9320,8 +9527,6 @@ impl LspStore {
true
}
});
- let _ = self.language_server_statuses.remove(&server_id);
- log::info!("stopping language server {name}");
self.buffer_store.update(cx, |buffer_store, cx| {
for buffer in buffer_store.buffers() {
buffer.update(cx, |buffer, cx| {
@@ -9367,19 +9572,85 @@ impl LspStore {
});
}
local.language_server_watched_paths.remove(&server_id);
+
let server_state = local.language_servers.remove(&server_id);
- cx.notify();
self.cleanup_lsp_data(server_id);
- cx.emit(LspStoreEvent::LanguageServerRemoved(server_id));
- cx.spawn(async move |_, cx| {
- Self::shutdown_language_server(server_state, name, cx).await;
- orphaned_worktrees
- })
+ let name = self
+ .language_server_statuses
+ .remove(&server_id)
+ .map(|status| LanguageServerName::from(status.name.as_str()))
+ .or_else(|| {
+ if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() {
+ Some(adapter.name())
+ } else {
+ None
+ }
+ });
+
+ if let Some(name) = name {
+ log::info!("stopping language server {name}");
+ self.languages
+ .update_lsp_binary_status(name.clone(), BinaryStatus::Stopping);
+ cx.notify();
+
+ return cx.spawn(async move |lsp_store, cx| {
+ Self::shutdown_language_server(server_state, name.clone(), cx).await;
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store
+ .languages
+ .update_lsp_binary_status(name, BinaryStatus::Stopped);
+ cx.emit(LspStoreEvent::LanguageServerRemoved(server_id));
+ cx.notify();
+ })
+ .ok();
+ orphaned_worktrees
+ });
+ }
+
+ if server_state.is_some() {
+ cx.emit(LspStoreEvent::LanguageServerRemoved(server_id));
+ }
+ Task::ready(orphaned_worktrees)
+ }
+
+ pub fn stop_all_language_servers(&mut self, cx: &mut Context<Self>) {
+ if let Some((client, project_id)) = self.upstream_client() {
+ let request = client.request(proto::StopLanguageServers {
+ project_id,
+ buffer_ids: Vec::new(),
+ also_servers: Vec::new(),
+ all: true,
+ });
+ cx.background_spawn(request).detach_and_log_err(cx);
+ } else {
+ let Some(local) = self.as_local_mut() else {
+ return;
+ };
+ let language_servers_to_stop = local
+ .language_server_ids
+ .values()
+ .flatten()
+ .copied()
+ .collect();
+ local.lsp_tree.update(cx, |this, _| {
+ this.remove_nodes(&language_servers_to_stop);
+ });
+ let tasks = language_servers_to_stop
+ .into_iter()
+ .map(|server| self.stop_local_language_server(server, cx))
+ .collect::<Vec<_>>();
+ cx.background_spawn(async move {
+ futures::future::join_all(tasks).await;
+ })
+ .detach();
+ }
}
pub fn restart_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
+ only_restart_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
) {
if let Some((client, project_id)) = self.upstream_client() {
@@ -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 ¶ms.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 ¶ms.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();
}
@@ -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 {
@@ -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)
})
}
@@ -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
@@ -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();
@@ -534,12 +534,15 @@ message DiagnosticSummary {
message UpdateLanguageServer {
uint64 project_id = 1;
uint64 language_server_id = 2;
+ optional string server_name = 8;
oneof variant {
LspWorkStart work_start = 3;
LspWorkProgress work_progress = 4;
LspWorkEnd work_end = 5;
LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6;
LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7;
+ StatusUpdate status_update = 9;
+ RegisteredForBuffer registered_for_buffer = 10;
}
}
@@ -566,6 +569,34 @@ message LspDiskBasedDiagnosticsUpdating {}
message LspDiskBasedDiagnosticsUpdated {}
+message StatusUpdate {
+ optional string message = 1;
+ oneof status {
+ ServerBinaryStatus binary = 2;
+ ServerHealth health = 3;
+ }
+}
+
+enum ServerHealth {
+ OK = 0;
+ WARNING = 1;
+ ERROR = 2;
+}
+
+enum ServerBinaryStatus {
+ NONE = 0;
+ CHECKING_FOR_UPDATE = 1;
+ DOWNLOADING = 2;
+ STARTING = 3;
+ STOPPING = 4;
+ STOPPED = 5;
+ FAILED = 6;
+}
+
+message RegisteredForBuffer {
+ string buffer_abs_path = 1;
+}
+
message LanguageServerLog {
uint64 project_id = 1;
uint64 language_server_id = 2;
@@ -593,6 +624,7 @@ message ApplyCodeActionKindResponse {
message RegisterBufferWithLanguageServers {
uint64 project_id = 1;
uint64 buffer_id = 2;
+ repeated LanguageServerSelector only_servers = 3;
}
enum FormatTrigger {
@@ -730,14 +762,25 @@ message MultiLspQuery {
message AllLanguageServers {}
+message LanguageServerSelector {
+ oneof selector {
+ uint64 server_id = 1;
+ string name = 2;
+ }
+}
+
message RestartLanguageServers {
uint64 project_id = 1;
repeated uint64 buffer_ids = 2;
+ repeated LanguageServerSelector only_servers = 3;
+ bool all = 4;
}
message StopLanguageServers {
uint64 project_id = 1;
repeated uint64 buffer_ids = 2;
+ repeated LanguageServerSelector also_servers = 3;
+ bool all = 4;
}
message MultiLspQueryResponse {
@@ -301,11 +301,13 @@ impl HeadlessProject {
match event {
LspStoreEvent::LanguageServerUpdate {
language_server_id,
+ name,
message,
} => {
self.session
.send(proto::UpdateLanguageServer {
project_id: SSH_PROJECT_ID,
+ server_name: name.as_ref().map(|name| name.to_string()),
language_server_id: language_server_id.to_proto(),
variant: Some(message.clone()),
})
@@ -5617,7 +5617,6 @@ impl Workspace {
} else if let Some((notification_id, _)) = self.notifications.pop() {
dismiss_app_notification(¬ification_id, cx);
} else {
- cx.emit(Event::ClearActivityIndicator);
cx.propagate();
}
}
@@ -30,6 +30,7 @@ use gpui::{
px, retain_all,
};
use image_viewer::ImageInfo;
+use language_tools::lsp_tool::LspTool;
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
use migrator::{migrate_keymap, migrate_settings};
pub use open_listener::*;
@@ -295,7 +296,7 @@ pub fn initialize_workspace(
let popover_menu_handle = PopoverMenuHandle::default();
- let inline_completion_button = cx.new(|cx| {
+ let edit_prediction_button = cx.new(|cx| {
inline_completion_button::InlineCompletionButton::new(
app_state.fs.clone(),
app_state.user_store.clone(),
@@ -315,7 +316,7 @@ pub fn initialize_workspace(
cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator = activity_indicator::ActivityIndicator::new(
workspace,
- app_state.languages.clone(),
+ workspace.project().read(cx).languages().clone(),
window,
cx,
);
@@ -325,13 +326,16 @@ pub fn initialize_workspace(
cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
let image_info = cx.new(|_cx| ImageInfo::new(workspace));
+ let lsp_tool = cx.new(|cx| LspTool::new(workspace, window, cx));
+
let cursor_position =
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(search_button, window, cx);
status_bar.add_left_item(diagnostic_summary, window, cx);
+ status_bar.add_left_item(lsp_tool, window, cx);
status_bar.add_left_item(activity_indicator, window, cx);
- status_bar.add_right_item(inline_completion_button, window, cx);
+ status_bar.add_right_item(edit_prediction_button, window, cx);
status_bar.add_right_item(active_buffer_language, window, cx);
status_bar.add_right_item(active_toolchain_language, window, cx);
status_bar.add_right_item(vim_mode_indicator, window, cx);
@@ -4300,6 +4304,7 @@ mod tests {
"jj",
"journal",
"language_selector",
+ "lsp_tool",
"markdown",
"menu",
"notebook",