@@ -17,8 +17,7 @@ use project::{
};
use settings::{Settings as _, SettingsStore};
use ui::{
- Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
- Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
+ ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::{ResultExt, rel_path::RelPath};
@@ -91,7 +90,7 @@ struct LanguageServerBinaryStatus {
message: Option<SharedString>,
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
struct ServerInfo {
name: LanguageServerName,
id: LanguageServerId,
@@ -104,6 +103,12 @@ impl ServerInfo {
fn server_selector(&self) -> LanguageServerSelector {
LanguageServerSelector::Id(self.id)
}
+
+ fn can_stop(&self) -> bool {
+ self.binary_status.as_ref().is_none_or(|status| {
+ matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
+ })
+ }
}
impl LanguageServerHealthStatus {
@@ -145,7 +150,9 @@ impl LanguageServerState {
} else {
"Stop All Servers"
};
+
let restart = *restart;
+
let button = ContextMenuEntry::new(label).handler({
let state = cx.entity();
move |_, cx| {
@@ -206,10 +213,12 @@ impl LanguageServerState {
.ok();
}
});
+
if !first_button_encountered {
menu = menu.separator();
first_button_encountered = true;
}
+
menu = menu.item(button);
continue;
} else if let LspMenuItem::Header { header, separator } = item {
@@ -229,165 +238,290 @@ impl LanguageServerState {
.unwrap_or(false);
let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
- let status_color = server_info
+ let (status_color, status_label) = server_info
.binary_status
.as_ref()
.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),
+ | BinaryStatus::Starting => Some((Color::Modified, "Starting…")),
+ BinaryStatus::Stopping | BinaryStatus::Stopped => {
+ Some((Color::Disabled, "Stopped"))
+ }
+ BinaryStatus::Failed { .. } => Some((Color::Error, "Error")),
})
.or_else(|| {
Some(match server_info.health? {
- ServerHealth::Ok => Color::Success,
- ServerHealth::Warning => Color::Warning,
- ServerHealth::Error => Color::Error,
+ ServerHealth::Ok => (Color::Success, "Running"),
+ ServerHealth::Warning => (Color::Warning, "Warning"),
+ ServerHealth::Error => (Color::Error, "Error"),
})
})
- .unwrap_or(Color::Success);
+ .unwrap_or((Color::Success, "Running"));
let message = server_info
.message
.as_ref()
.or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
.cloned();
- let hover_label = if message.is_some() {
- Some("View Message")
- } else if has_logs {
- Some("View Logs")
- } else {
- None
- };
- let server_name = server_info.name.clone();
let server_version = server_versions
.get(&server_info.id)
.and_then(|version| version.clone());
- let tooltip_text = match (&server_version, &message) {
+ let metadata_label = match (&server_version, &message) {
(None, None) => None,
- (Some(version), None) => {
- Some(SharedString::from(format!("Version: {}", version.as_ref())))
- }
+ (Some(version), None) => Some(SharedString::from(format!("v{}", version.as_ref()))),
(None, Some(message)) => Some(message.clone()),
(Some(version), Some(message)) => Some(SharedString::from(format!(
- "Version: {}\n\n{}",
+ "v{}\n\n{}",
version.as_ref(),
message.as_ref()
))),
};
- menu = menu.item(ContextMenuItem::custom_entry(
- move |_, _| {
- h_flex()
- .group("menu_item")
- .w_full()
- .gap_2()
- .justify_between()
- .child(
- h_flex()
- .gap_2()
- .child(Indicator::dot().color(status_color))
- .child(Label::new(server_name.0.clone())),
- )
- .when_some(hover_label, |div, hover_label| {
- div.child(
- h_flex()
- .visible_on_hover("menu_item")
- .child(
- Label::new(hover_label)
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(
- Icon::new(IconName::ChevronRight)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
- )
- })
- .into_any_element()
- },
+
+ let submenu_server_name = server_info.name.clone();
+ let submenu_server_info = server_info.clone();
+
+ menu = menu.submenu_with_colored_icon(
+ server_info.name.0.clone(),
+ IconName::Circle,
+ status_color,
{
let lsp_logs = lsp_logs.clone();
let message = message.clone();
let server_selector = server_selector.clone();
- let server_name = server_info.name.clone();
let workspace = self.workspace.clone();
- move |window, cx| {
- if let Some(message) = &message {
- let Some(create_buffer) = workspace
- .update(cx, |workspace, cx| {
- workspace
- .project()
- .update(cx, |project, cx| project.create_buffer(false, cx))
+ let lsp_store = self.lsp_store.clone();
+ let state = cx.entity().downgrade();
+ let can_stop = submenu_server_info.can_stop();
+
+ move |menu, _window, _cx| {
+ let mut submenu = menu;
+
+ if let Some(ref message) = message {
+ let workspace_for_message = workspace.clone();
+ let message_for_handler = message.clone();
+ let server_name_for_message = submenu_server_name.clone();
+ submenu = submenu.entry("View Message", None, move |window, cx| {
+ let Some(create_buffer) = workspace_for_message
+ .update(cx, |workspace, cx| {
+ workspace.project().update(cx, |project, cx| {
+ project.create_buffer(false, cx)
+ })
+ })
+ .ok()
+ else {
+ return;
+ };
+
+ let window_handle = window.window_handle();
+ let workspace = workspace_for_message.clone();
+ let message = message_for_handler.clone();
+ let server_name = server_name_for_message.clone();
+ cx.spawn(async move |cx| {
+ let buffer = create_buffer.await?;
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [(
+ 0..0,
+ format!(
+ "Language server {server_name}:\n\n{message}"
+ ),
+ )],
+ None,
+ cx,
+ );
+ buffer.set_capability(language::Capability::ReadOnly, cx);
+ })?;
+
+ workspace.update(cx, |workspace, cx| {
+ window_handle.update(cx, |_, window, cx| {
+ workspace.add_item_to_active_pane(
+ Box::new(cx.new(|cx| {
+ let mut editor = Editor::for_buffer(
+ buffer, None, window, cx,
+ );
+ editor.set_read_only(true);
+ editor
+ })),
+ None,
+ true,
+ window,
+ cx,
+ );
+ })
+ })??;
+
+ anyhow::Ok(())
})
- .ok()
- else {
+ .detach();
+ });
+ }
+
+ if has_logs {
+ let lsp_logs_for_debug = lsp_logs.clone();
+ let workspace_for_debug = workspace.clone();
+ let server_selector_for_debug = server_selector.clone();
+ submenu = submenu.entry("View Logs", None, move |window, cx| {
+ lsp_log_view::open_server_trace(
+ &lsp_logs_for_debug,
+ workspace_for_debug.clone(),
+ server_selector_for_debug.clone(),
+ window,
+ cx,
+ );
+ });
+ }
+
+ let state_for_restart = state.clone();
+ let workspace_for_restart = workspace.clone();
+ let lsp_store_for_restart = lsp_store.clone();
+ let server_name_for_restart = submenu_server_name.clone();
+ submenu = submenu.entry("Restart Server", None, move |_window, cx| {
+ let Some(workspace) = workspace_for_restart.upgrade() else {
return;
};
- let window = window.window_handle();
- let workspace = workspace.clone();
- let message = message.clone();
- let server_name = server_name.clone();
- cx.spawn(async move |cx| {
- let buffer = create_buffer.await?;
- buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(
- 0..0,
- format!("Language server {server_name}:\n\n{message}"),
- )],
- None,
- cx,
- );
- buffer.set_capability(language::Capability::ReadOnly, cx);
- })?;
-
- workspace.update(cx, |workspace, cx| {
- window.update(cx, |_, window, cx| {
- workspace.add_item_to_active_pane(
- Box::new(cx.new(|cx| {
- let mut editor =
- Editor::for_buffer(buffer, None, window, cx);
- editor.set_read_only(true);
- editor
- })),
- None,
- true,
- window,
+ let project = workspace.read(cx).project().clone();
+ let path_style = project.read(cx).path_style(cx);
+ let buffer_store = project.read(cx).buffer_store().clone();
+
+ let buffers = state_for_restart
+ .update(cx, |state, cx| {
+ let server_buffers = state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .iter()
+ .filter_map(|(abs_path, servers)| {
+ // Check if this server is associated with this path
+ let has_server = servers.servers.values().any(|name| {
+ name.as_ref() == Some(&server_name_for_restart)
+ });
+
+ if !has_server {
+ return None;
+ }
+
+ let worktree = servers.worktree.as_ref()?.upgrade()?;
+ let worktree_ref = worktree.read(cx);
+ let relative_path = abs_path
+ .strip_prefix(&worktree_ref.abs_path())
+ .ok()?;
+ let relative_path =
+ RelPath::new(relative_path, path_style)
+ .log_err()?;
+ let entry =
+ worktree_ref.entry_for_path(&relative_path)?;
+ let project_path =
+ project.read(cx).path_for_entry(entry.id, cx)?;
+
+ buffer_store.read(cx).get_by_path(&project_path)
+ })
+ .collect::<Vec<_>>();
+
+ if server_buffers.is_empty() {
+ state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .iter()
+ .filter_map(|(abs_path, servers)| {
+ let worktree =
+ servers.worktree.as_ref()?.upgrade()?.read(cx);
+ let relative_path = abs_path
+ .strip_prefix(&worktree.abs_path())
+ .ok()?;
+ let relative_path =
+ RelPath::new(relative_path, path_style)
+ .log_err()?;
+ let entry =
+ worktree.entry_for_path(&relative_path)?;
+ let project_path = project
+ .read(cx)
+ .path_for_entry(entry.id, cx)?;
+ buffer_store.read(cx).get_by_path(&project_path)
+ })
+ .collect()
+ } else {
+ server_buffers
+ }
+ })
+ .unwrap_or_default();
+
+ if !buffers.is_empty() {
+ lsp_store_for_restart
+ .update(cx, |lsp_store, cx| {
+ lsp_store.restart_language_servers_for_buffers(
+ buffers,
+ HashSet::from_iter([LanguageServerSelector::Name(
+ server_name_for_restart.clone(),
+ )]),
cx,
);
})
- })??;
+ .ok();
+ }
+ });
- anyhow::Ok(())
- })
- .detach();
- } else if has_logs {
- lsp_log_view::open_server_trace(
- &lsp_logs,
- workspace.clone(),
- server_selector.clone(),
- window,
- cx,
- );
- } else {
- cx.propagate();
+ if can_stop {
+ let lsp_store_for_stop = lsp_store.clone();
+ let server_selector_for_stop = server_selector.clone();
+
+ submenu = submenu.entry("Stop Server", None, move |_window, cx| {
+ lsp_store_for_stop
+ .update(cx, |lsp_store, cx| {
+ lsp_store
+ .stop_language_servers_for_buffers(
+ Vec::new(),
+ HashSet::from_iter([
+ server_selector_for_stop.clone()
+ ]),
+ cx,
+ )
+ .detach_and_log_err(cx);
+ })
+ .ok();
+ });
}
+
+ submenu = submenu.separator().custom_row({
+ let metadata_label = metadata_label.clone();
+ move |_, _| {
+ h_flex()
+ .ml_neg_1()
+ .gap_1()
+ .child(
+ Icon::new(IconName::Circle)
+ .color(status_color)
+ .size(IconSize::Small),
+ )
+ .child(
+ Label::new(status_label)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .when_some(metadata_label.as_ref(), |submenu, metadata| {
+ submenu
+ .child(
+ Icon::new(IconName::Dash)
+ .color(Color::Disabled)
+ .size(IconSize::XSmall),
+ )
+ .child(
+ Label::new(metadata)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ })
+ .into_any_element()
+ }
+ });
+
+ submenu
}
},
- tooltip_text.map(|tooltip_text| {
- DocumentationAside::new(
- DocumentationSide::Right,
- Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
- )
- }),
- ));
+ );
}
menu
}
@@ -27,7 +27,6 @@ struct OpenSubmenu {
item_index: usize,
entity: Entity<ContextMenu>,
trigger_bounds: Option<Bounds<Pixels>>,
- // Capture the submenu's vertical offset once and keep it stable while the submenu is open.
offset: Option<Pixels>,
_dismiss_subscription: Subscription,
}
@@ -61,6 +60,7 @@ pub enum ContextMenuItem {
Submenu {
label: SharedString,
icon: Option<IconName>,
+ icon_color: Option<Color>,
builder: Rc<dyn Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu>,
},
}
@@ -763,6 +763,7 @@ impl ContextMenu {
self.items.push(ContextMenuItem::Submenu {
label: label.into(),
icon: None,
+ icon_color: None,
builder: Rc::new(builder),
});
self
@@ -777,6 +778,23 @@ impl ContextMenu {
self.items.push(ContextMenuItem::Submenu {
label: label.into(),
icon: Some(icon),
+ icon_color: None,
+ builder: Rc::new(builder),
+ });
+ self
+ }
+
+ pub fn submenu_with_colored_icon(
+ mut self,
+ label: impl Into<SharedString>,
+ icon: IconName,
+ icon_color: Color,
+ builder: impl Fn(ContextMenu, &mut Window, &mut Context<ContextMenu>) -> ContextMenu + 'static,
+ ) -> Self {
+ self.items.push(ContextMenuItem::Submenu {
+ label: label.into(),
+ icon: Some(icon),
+ icon_color: Some(icon_color),
builder: Rc::new(builder),
});
self
@@ -1319,8 +1337,13 @@ impl ContextMenu {
)
.into_any_element()
}
- ContextMenuItem::Submenu { label, icon, .. } => self
- .render_submenu_item_trigger(ix, label.clone(), *icon, cx)
+ ContextMenuItem::Submenu {
+ label,
+ icon,
+ icon_color,
+ ..
+ } => self
+ .render_submenu_item_trigger(ix, label.clone(), *icon, *icon_color, cx)
.into_any_element(),
}
}
@@ -1330,6 +1353,7 @@ impl ContextMenu {
ix: usize,
label: SharedString,
icon: Option<IconName>,
+ icon_color: Option<Color>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let toggle_state = Some(ix) == self.selected_index
@@ -1413,9 +1437,17 @@ impl ContextMenu {
SubmenuState::Open(open_submenu) if open_submenu.item_index == ix
);
- if is_open_for_this_item && this.hover_target != HoverTarget::Submenu {
+ let mouse_in_submenu_zone = this
+ .padded_submenu_bounds()
+ .is_some_and(|bounds| bounds.contains(&window.mouse_position()));
+
+ if is_open_for_this_item
+ && this.hover_target != HoverTarget::Submenu
+ && !mouse_in_submenu_zone
+ {
this.close_submenu(false, cx);
this.clear_selected();
+ window.focus(&this.focus_handle.clone(), cx);
cx.notify();
}
}
@@ -1441,6 +1473,7 @@ impl ContextMenu {
.child(
h_flex()
.w_full()
+ .gap_2()
.justify_between()
.child(
h_flex()
@@ -1449,7 +1482,7 @@ impl ContextMenu {
this.child(
Icon::new(icon_name)
.size(IconSize::Small)
- .color(Color::Default),
+ .color(icon_color.unwrap_or(Color::Muted)),
)
})
.child(Label::new(label).color(Color::Default)),
@@ -1973,9 +2006,14 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::select_submenu_parent))
.on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel))
- .on_hover(cx.listener(|this, hovered: &bool, _, _| {
+ .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
if *hovered {
this.hover_target = HoverTarget::MainMenu;
+ if let Some(parent) = &this.main_menu {
+ parent.update(cx, |parent, _| {
+ parent.hover_target = HoverTarget::Submenu;
+ });
+ }
}
}))
.on_mouse_down_out(cx.listener(