lsp: Add server-side tracing support (#15230)

This PR adds another row to the LSP log view: Server traces

![image](https://github.com/user-attachments/assets/e3f77944-45e0-4d04-92fd-aea212859e86)


[Traces](https://docs.rs/lsp-types/latest/lsp_types/notification/enum.LogTrace.html)
are intended for logging execution diagnostics, which is different from
`LogMessage` that we currently support.
When `Server trace` is selected, user can select the level of tracing
(`off`/`messages`/`verbose`) to their liking.

Release Notes:

- Added support for language server tracing to the LSP log view.
This commit is contained in:
Piotr Osiewicz 2024-07-29 01:53:30 +02:00 committed by GitHub
parent bb188f673e
commit a875dd153d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 301 additions and 139 deletions

View file

@ -8,7 +8,7 @@ use gpui::{
ViewContext, VisualContext, WeakModel, WindowContext, ViewContext, VisualContext, WeakModel, WindowContext,
}; };
use language::{LanguageServerId, LanguageServerName}; use language::{LanguageServerId, LanguageServerName};
use lsp::{IoKind, LanguageServer}; use lsp::{notification::SetTrace, IoKind, LanguageServer, SetTraceParams, TraceValue};
use project::{search::SearchQuery, Project}; use project::{search::SearchQuery, Project};
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection}; use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
@ -37,9 +37,12 @@ struct ProjectState {
struct LanguageServerState { struct LanguageServerState {
kind: LanguageServerKind, kind: LanguageServerKind,
log_messages: VecDeque<String>, log_messages: VecDeque<String>,
trace_messages: VecDeque<String>,
rpc_state: Option<LanguageServerRpcState>, rpc_state: Option<LanguageServerRpcState>,
trace_level: TraceValue,
_io_logs_subscription: Option<lsp::Subscription>, _io_logs_subscription: Option<lsp::Subscription>,
_lsp_logs_subscription: Option<lsp::Subscription>, _lsp_logs_subscription: Option<lsp::Subscription>,
_lsp_trace_subscription: Option<lsp::Subscription>,
} }
enum LanguageServerKind { enum LanguageServerKind {
@ -66,7 +69,7 @@ pub struct LspLogView {
editor_subscriptions: Vec<Subscription>, editor_subscriptions: Vec<Subscription>,
log_store: Model<LogStore>, log_store: Model<LogStore>,
current_server_id: Option<LanguageServerId>, current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool, active_entry_kind: LogKind,
project: Model<Project>, project: Model<Project>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
_log_store_subscriptions: Vec<Subscription>, _log_store_subscriptions: Vec<Subscription>,
@ -83,14 +86,32 @@ enum MessageKind {
Receive, Receive,
} }
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum LogKind {
Rpc,
Trace,
#[default]
Logs,
}
impl LogKind {
fn label(&self) -> &'static str {
match self {
LogKind::Rpc => RPC_MESSAGES,
LogKind::Trace => SERVER_TRACE,
LogKind::Logs => SERVER_LOGS,
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub(crate) struct LogMenuItem { pub(crate) struct LogMenuItem {
pub server_id: LanguageServerId, pub server_id: LanguageServerId,
pub server_name: LanguageServerName, pub server_name: LanguageServerName,
pub worktree_root_name: String, pub worktree_root_name: String,
pub rpc_trace_enabled: bool, pub rpc_trace_enabled: bool,
pub rpc_trace_selected: bool, pub selected_entry: LogKind,
pub logs_selected: bool, pub trace_level: lsp::TraceValue,
} }
actions!(debug, [OpenLanguageServerLogs]); actions!(debug, [OpenLanguageServerLogs]);
@ -244,12 +265,17 @@ impl LogStore {
kind, kind,
rpc_state: None, rpc_state: None,
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_level: TraceValue::Off,
_io_logs_subscription: None, _io_logs_subscription: None,
_lsp_logs_subscription: None, _lsp_logs_subscription: None,
_lsp_trace_subscription: None,
} }
}); });
if server.has_notification_handler::<lsp::notification::LogMessage>() { if server.has_notification_handler::<lsp::notification::LogMessage>()
|| server.has_notification_handler::<lsp::notification::LogTrace>()
{
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
return Some(server_state); return Some(server_state);
} }
@ -264,6 +290,7 @@ impl LogStore {
let this = cx.handle().downgrade(); let this = cx.handle().downgrade();
server_state._lsp_logs_subscription = server_state._lsp_logs_subscription =
Some(server.on_notification::<lsp::notification::LogMessage, _>({ Some(server.on_notification::<lsp::notification::LogMessage, _>({
let this = this.clone();
move |params, mut cx| { move |params, mut cx| {
if let Some(this) = this.upgrade() { if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
@ -273,6 +300,17 @@ impl LogStore {
} }
} }
})); }));
server_state._lsp_trace_subscription =
Some(server.on_notification::<lsp::notification::LogTrace, _>({
move |params, mut cx| {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.add_language_server_trace(server_id, &params.message, cx);
})
.ok();
}
}
}));
Some(server_state) Some(server_state)
} }
@ -285,6 +323,30 @@ impl LogStore {
let language_server_state = self.get_language_server_state(id)?; let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.log_messages; let log_lines = &mut language_server_state.log_messages;
Self::add_language_server_message(log_lines, id, message, LogKind::Logs, cx);
Some(())
}
fn add_language_server_trace(
&mut self,
id: LanguageServerId,
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.trace_messages;
Self::add_language_server_message(log_lines, id, message, LogKind::Trace, cx);
Some(())
}
fn add_language_server_message(
log_lines: &mut VecDeque<String>,
id: LanguageServerId,
message: &str,
kind: LogKind,
cx: &mut ModelContext<Self>,
) {
while log_lines.len() >= MAX_STORED_LOG_ENTRIES { while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front(); log_lines.pop_front();
} }
@ -293,10 +355,9 @@ impl LogStore {
cx.emit(Event::NewServerLogEntry { cx.emit(Event::NewServerLogEntry {
id, id,
entry: message.to_string(), entry: message.to_string(),
is_rpc: false, kind,
}); });
cx.notify(); cx.notify();
Some(())
} }
fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext<Self>) { fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext<Self>) {
@ -307,7 +368,9 @@ impl LogStore {
fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> { fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> {
Some(&self.language_servers.get(&server_id)?.log_messages) Some(&self.language_servers.get(&server_id)?.log_messages)
} }
fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> {
Some(&self.language_servers.get(&server_id)?.trace_messages)
}
fn server_ids_for_project<'a>( fn server_ids_for_project<'a>(
&'a self, &'a self,
lookup_project: &'a WeakModel<Project>, lookup_project: &'a WeakModel<Project>,
@ -386,7 +449,7 @@ impl LogStore {
cx.emit(Event::NewServerLogEntry { cx.emit(Event::NewServerLogEntry {
id: language_server_id, id: language_server_id,
entry: line_before_message.to_string(), entry: line_before_message.to_string(),
is_rpc: true, kind: LogKind::Rpc,
}); });
} }
@ -398,7 +461,7 @@ impl LogStore {
cx.emit(Event::NewServerLogEntry { cx.emit(Event::NewServerLogEntry {
id: language_server_id, id: language_server_id,
entry: message.to_string(), entry: message.to_string(),
is_rpc: true, kind: LogKind::Rpc,
}); });
cx.notify(); cx.notify();
Some(()) Some(())
@ -425,10 +488,10 @@ impl LspLogView {
if let Some(current_lsp) = this.current_server_id { if let Some(current_lsp) = this.current_server_id {
if !store.read(cx).language_servers.contains_key(&current_lsp) { if !store.read(cx).language_servers.contains_key(&current_lsp) {
if let Some(server_id) = first_server_id_for_project { if let Some(server_id) = first_server_id_for_project {
if this.is_showing_rpc_trace { match this.active_entry_kind {
this.show_rpc_trace_for_server(server_id, cx) LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
} else { LogKind::Trace => this.show_trace_for_server(server_id, cx),
this.show_logs_for_server(server_id, cx) LogKind::Logs => this.show_logs_for_server(server_id, cx),
} }
} else { } else {
this.current_server_id = None; this.current_server_id = None;
@ -441,21 +504,19 @@ impl LspLogView {
} }
} }
} else if let Some(server_id) = first_server_id_for_project { } else if let Some(server_id) = first_server_id_for_project {
if this.is_showing_rpc_trace { match this.active_entry_kind {
this.show_rpc_trace_for_server(server_id, cx) LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
} else { LogKind::Trace => this.show_trace_for_server(server_id, cx),
this.show_logs_for_server(server_id, cx) LogKind::Logs => this.show_logs_for_server(server_id, cx),
} }
} }
cx.notify(); cx.notify();
}); });
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
Event::NewServerLogEntry { id, entry, is_rpc } => { Event::NewServerLogEntry { id, entry, kind } => {
if log_view.current_server_id == Some(*id) { if log_view.current_server_id == Some(*id) {
if (*is_rpc && log_view.is_showing_rpc_trace) if *kind == log_view.active_entry_kind {
|| (!*is_rpc && !log_view.is_showing_rpc_trace)
{
log_view.editor.update(cx, |editor, cx| { log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false); editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx); let last_point = editor.buffer().read(cx).len(cx);
@ -486,7 +547,7 @@ impl LspLogView {
project, project,
log_store, log_store,
current_server_id: None, current_server_id: None,
is_showing_rpc_trace: false, active_entry_kind: LogKind::Logs,
_log_store_subscriptions: vec![ _log_store_subscriptions: vec![
model_changes_subscription, model_changes_subscription,
events_subscriptions, events_subscriptions,
@ -541,10 +602,8 @@ impl LspLogView {
server_name: language_server_name, server_name: language_server_name,
worktree_root_name: worktree.read(cx).root_name().to_string(), worktree_root_name: worktree.read(cx).root_name().to_string(),
rpc_trace_enabled: state.rpc_state.is_some(), rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace selected_entry: self.active_entry_kind,
&& self.current_server_id == Some(server_id), trace_level: lsp::TraceValue::Off,
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
}) })
}) })
.chain( .chain(
@ -558,10 +617,8 @@ impl LspLogView {
server_name: name.clone(), server_name: name.clone(),
worktree_root_name: "supplementary".to_string(), worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(), rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace selected_entry: self.active_entry_kind,
&& self.current_server_id == Some(server_id), trace_level: lsp::TraceValue::Off,
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
}) })
}), }),
) )
@ -575,10 +632,8 @@ impl LspLogView {
server_name: name.clone(), server_name: name.clone(),
worktree_root_name: "supplementary".to_string(), worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(), rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace selected_entry: self.active_entry_kind,
&& self.current_server_id == Some(*server_id), trace_level: lsp::TraceValue::Off,
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(*server_id),
}), }),
_ => None, _ => None,
}), }),
@ -597,7 +652,23 @@ impl LspLogView {
.map(log_contents); .map(log_contents);
if let Some(log_contents) = log_contents { if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id); self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = false; self.active_entry_kind = LogKind::Logs;
let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
self.editor = editor;
self.editor_subscriptions = editor_subscriptions;
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let log_contents = self
.log_store
.read(cx)
.server_trace(server_id)
.map(log_contents);
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.active_entry_kind = LogKind::Trace;
let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx); let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
self.editor = editor; self.editor = editor;
self.editor_subscriptions = editor_subscriptions; self.editor_subscriptions = editor_subscriptions;
@ -618,7 +689,7 @@ impl LspLogView {
}); });
if let Some(rpc_log) = rpc_log { if let Some(rpc_log) = rpc_log {
self.current_server_id = Some(server_id); self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = true; self.active_entry_kind = LogKind::Rpc;
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx); let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx);
let language = self.project.read(cx).languages().language_for_name("JSON"); let language = self.project.read(cx).languages().language_for_name("JSON");
editor editor
@ -666,6 +737,24 @@ impl LspLogView {
cx.notify(); cx.notify();
} }
} }
fn update_trace_level(
&self,
server_id: LanguageServerId,
level: TraceValue,
cx: &mut ViewContext<Self>,
) {
if let Some(server) = self.project.read(cx).language_server_for_id(server_id) {
self.log_store.update(cx, |this, _| {
if let Some(state) = this.get_language_server_state(server_id) {
state.trace_level = level;
}
});
server
.notify::<SetTrace>(SetTraceParams { value: level })
.ok();
}
}
} }
fn log_contents(lines: &VecDeque<String>) -> String { fn log_contents(lines: &VecDeque<String>) -> String {
@ -826,123 +915,195 @@ impl Render for LspLogToolbarItemView {
"{} ({}) - {}", "{} ({}) - {}",
row.server_name.0, row.server_name.0,
row.worktree_root_name, row.worktree_root_name,
if row.rpc_trace_selected { row.selected_entry.label()
RPC_MESSAGES
} else {
SERVER_LOGS
},
)) ))
}) })
.unwrap_or_else(|| "No server selected".into()), .unwrap_or_else(|| "No server selected".into()),
)) ))
.menu(move |cx| { .menu({
let menu_rows = menu_rows.clone();
let log_view = log_view.clone(); let log_view = log_view.clone();
let log_toolbar_view = log_toolbar_view.clone(); move |cx| {
ContextMenu::build(cx, move |mut menu, cx| { let menu_rows = menu_rows.clone();
for (ix, row) in menu_rows.into_iter().enumerate() { let log_view = log_view.clone();
let server_selected = Some(row.server_id) == current_server_id; let log_toolbar_view = log_toolbar_view.clone();
menu = menu ContextMenu::build(cx, move |mut menu, cx| {
.header(format!( for (ix, row) in menu_rows.into_iter().enumerate() {
"{} ({})", let server_selected = Some(row.server_id) == current_server_id;
row.server_name.0, row.worktree_root_name menu = menu
)) .header(format!(
.entry( "{} ({})",
SERVER_LOGS, row.server_name.0, row.worktree_root_name
))
.entry(
SERVER_LOGS,
None,
cx.handler_for(&log_view, move |view, cx| {
view.show_logs_for_server(row.server_id, cx);
}),
);
if server_selected && row.selected_entry == LogKind::Logs {
let selected_ix = menu.select_last();
debug_assert_eq!(
Some(ix * 4 + 1),
selected_ix,
"Could not scroll to a just added LSP menu item"
);
}
menu = menu.entry(
SERVER_TRACE,
None, None,
cx.handler_for(&log_view, move |view, cx| { cx.handler_for(&log_view, move |view, cx| {
view.show_logs_for_server(row.server_id, cx); view.show_trace_for_server(row.server_id, cx);
}), }),
); );
if server_selected && row.logs_selected { if server_selected && row.selected_entry == LogKind::Trace {
let selected_ix = menu.select_last(); let selected_ix = menu.select_last();
debug_assert_eq!( debug_assert_eq!(
Some(ix * 3 + 1), Some(ix * 4 + 2),
selected_ix, selected_ix,
"Could not scroll to a just added LSP menu item" "Could not scroll to a just added LSP menu item"
); );
} }
menu = menu.custom_entry(
menu = menu.custom_entry( {
{ let log_toolbar_view = log_toolbar_view.clone();
let log_toolbar_view = log_toolbar_view.clone(); move |cx| {
move |cx| { h_flex()
h_flex() .w_full()
.w_full() .justify_between()
.justify_between() .child(Label::new(RPC_MESSAGES))
.child(Label::new(RPC_MESSAGES)) .child(
.child( div().child(
div().child( Checkbox::new(
Checkbox::new( ix,
ix, if row.rpc_trace_enabled {
if row.rpc_trace_enabled {
Selection::Selected
} else {
Selection::Unselected
},
)
.on_click(cx.listener_for(
&log_toolbar_view,
move |view, selection, cx| {
let enabled = matches!(
selection,
Selection::Selected Selection::Selected
); } else {
view.toggle_rpc_logging_for_server( Selection::Unselected
row.server_id, },
enabled, )
cx, .on_click(cx.listener_for(
); &log_toolbar_view,
cx.stop_propagation(); move |view, selection, cx| {
}, let enabled = matches!(
)), selection,
), Selection::Selected
) );
.into_any_element() view.toggle_rpc_logging_for_server(
} row.server_id,
}, enabled,
cx.handler_for(&log_view, move |view, cx| { cx,
view.show_rpc_trace_for_server(row.server_id, cx); );
}), cx.stop_propagation();
); },
if server_selected && row.rpc_trace_selected { )),
let selected_ix = menu.select_last(); ),
debug_assert_eq!( )
Some(ix * 3 + 2), .into_any_element()
selected_ix, }
"Could not scroll to a just added LSP menu item" },
cx.handler_for(&log_view, move |view, cx| {
view.show_rpc_trace_for_server(row.server_id, cx);
}),
); );
if server_selected && row.selected_entry == LogKind::Rpc {
let selected_ix = menu.select_last();
debug_assert_eq!(
Some(ix * 4 + 3),
selected_ix,
"Could not scroll to a just added LSP menu item"
);
}
} }
} menu
menu })
}) .into()
.into() }
}); });
h_flex().size_full().child(lsp_menu).child( h_flex()
div() .size_full()
.child( .child(lsp_menu)
Button::new("clear_log_button", "Clear").on_click(cx.listener( .child(
|this, _, cx| { div()
if let Some(log_view) = this.log_view.as_ref() { .child(
log_view.update(cx, |log_view, cx| { Button::new("clear_log_button", "Clear").on_click(cx.listener(
log_view.editor.update(cx, |editor, cx| { |this, _, cx| {
editor.set_read_only(false); if let Some(log_view) = this.log_view.as_ref() {
editor.clear(cx); log_view.update(cx, |log_view, cx| {
editor.set_read_only(true); log_view.editor.update(cx, |editor, cx| {
}); editor.set_read_only(false);
}) editor.clear(cx);
} editor.set_read_only(true);
}, });
)), })
) }
.ml_2(), },
) )),
)
.ml_2(),
)
.child(log_view.update(cx, |this, _| {
if this.active_entry_kind == LogKind::Trace {
let log_view = log_view.clone();
div().child(
PopoverMenu::new("lsp-trace-level-menu")
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_trace_level_selector",
"Trace level",
))
.menu({
let log_view = log_view.clone();
move |cx| {
let id = log_view.read(cx).current_server_id?;
let trace_level = log_view.update(cx, |this, cx| {
this.log_store.update(cx, |this, _| {
Some(this.get_language_server_state(id)?.trace_level)
})
})?;
ContextMenu::build(cx, |mut menu, _| {
let log_view = log_view.clone();
for (option, label) in [
(TraceValue::Off, "Off"),
(TraceValue::Messages, "Messages"),
(TraceValue::Verbose, "Verbose"),
] {
menu = menu.entry(label, None, {
let log_view = log_view.clone();
move |cx| {
log_view.update(cx, |this, cx| {
if let Some(id) = this.current_server_id {
this.update_trace_level(id, option, cx);
}
});
}
});
if option == trace_level {
menu.select_last();
}
}
menu
})
.into()
}
}),
)
} else {
div()
}
}))
} }
} }
const RPC_MESSAGES: &str = "RPC Messages"; const RPC_MESSAGES: &str = "RPC Messages";
const SERVER_LOGS: &str = "Server Logs"; const SERVER_LOGS: &str = "Server Logs";
const SERVER_TRACE: &str = "Server Trace";
impl LspLogToolbarItemView { impl LspLogToolbarItemView {
pub fn new() -> Self { pub fn new() -> Self {
@ -979,7 +1140,7 @@ pub enum Event {
NewServerLogEntry { NewServerLogEntry {
id: LanguageServerId, id: LanguageServerId,
entry: String, entry: String,
is_rpc: bool, kind: LogKind,
}, },
} }

View file

@ -8,6 +8,7 @@ use gpui::{Context, SemanticVersion, TestAppContext, VisualTestContext};
use language::{ use language::{
tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName, tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName,
}; };
use lsp_log::LogKind;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
@ -92,8 +93,8 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
.root_name() .root_name()
.to_string(), .to_string(),
rpc_trace_enabled: false, rpc_trace_enabled: false,
rpc_trace_selected: false, selected_entry: LogKind::Logs,
logs_selected: true, trace_level: lsp::TraceValue::Off,
}] }]
); );
assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n"); assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");

View file

@ -52,7 +52,7 @@ impl FluentBuilder for ContextMenu {}
impl ContextMenu { impl ContextMenu {
pub fn build( pub fn build(
cx: &mut WindowContext, cx: &mut WindowContext,
f: impl FnOnce(Self, &mut WindowContext) -> Self, f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
) -> View<Self> { ) -> View<Self> {
cx.new_view(|cx| { cx.new_view(|cx| {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();