lsp_log.rs

  1use collections::HashMap;
  2use editor::Editor;
  3use futures::{channel::mpsc, StreamExt};
  4use gpui::{
  5    actions,
  6    elements::{
  7        AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
  8        ParentElement, Stack,
  9    },
 10    platform::{CursorStyle, MouseButton},
 11    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
 12    ViewHandle, WeakModelHandle,
 13};
 14use language::{Buffer, LanguageServerId, LanguageServerName};
 15use lsp::IoKind;
 16use project::{Project, Worktree};
 17use std::{borrow::Cow, sync::Arc};
 18use theme::{ui, Theme};
 19use workspace::{
 20    item::{Item, ItemHandle},
 21    searchable::{SearchableItem, SearchableItemHandle},
 22    ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
 23};
 24
 25const SEND_LINE: &str = "// Send:\n";
 26const RECEIVE_LINE: &str = "// Receive:\n";
 27
 28pub struct LogStore {
 29    projects: HashMap<WeakModelHandle<Project>, ProjectState>,
 30    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, IoKind, String)>,
 31}
 32
 33struct ProjectState {
 34    servers: HashMap<LanguageServerId, LanguageServerState>,
 35    _subscriptions: [gpui::Subscription; 2],
 36}
 37
 38struct LanguageServerState {
 39    log_buffer: ModelHandle<Buffer>,
 40    rpc_state: Option<LanguageServerRpcState>,
 41    _subscription: Option<lsp::Subscription>,
 42}
 43
 44struct LanguageServerRpcState {
 45    buffer: ModelHandle<Buffer>,
 46    last_message_kind: Option<MessageKind>,
 47}
 48
 49pub struct LspLogView {
 50    pub(crate) editor: ViewHandle<Editor>,
 51    log_store: ModelHandle<LogStore>,
 52    current_server_id: Option<LanguageServerId>,
 53    is_showing_rpc_trace: bool,
 54    project: ModelHandle<Project>,
 55}
 56
 57pub struct LspLogToolbarItemView {
 58    log_view: Option<ViewHandle<LspLogView>>,
 59    menu_open: bool,
 60}
 61
 62#[derive(Copy, Clone, PartialEq, Eq)]
 63enum MessageKind {
 64    Send,
 65    Receive,
 66}
 67
 68#[derive(Clone, Debug, PartialEq)]
 69pub(crate) struct LogMenuItem {
 70    pub server_id: LanguageServerId,
 71    pub server_name: LanguageServerName,
 72    pub worktree: ModelHandle<Worktree>,
 73    pub rpc_trace_enabled: bool,
 74    pub rpc_trace_selected: bool,
 75    pub logs_selected: bool,
 76}
 77
 78actions!(debug, [OpenLanguageServerLogs]);
 79
 80pub fn init(cx: &mut AppContext) {
 81    let log_store = cx.add_model(|cx| LogStore::new(cx));
 82
 83    cx.subscribe_global::<WorkspaceCreated, _>({
 84        let log_store = log_store.clone();
 85        move |event, cx| {
 86            let workspace = &event.0;
 87            if let Some(workspace) = workspace.upgrade(cx) {
 88                let project = workspace.read(cx).project().clone();
 89                if project.read(cx).is_local() {
 90                    log_store.update(cx, |store, cx| {
 91                        store.add_project(&project, cx);
 92                    });
 93                }
 94            }
 95        }
 96    })
 97    .detach();
 98
 99    cx.add_action(
100        move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
101            let project = workspace.project().read(cx);
102            if project.is_local() {
103                workspace.add_item(
104                    Box::new(cx.add_view(|cx| {
105                        LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
106                    })),
107                    cx,
108                );
109            }
110        },
111    );
112}
113
114impl LogStore {
115    pub fn new(cx: &mut ModelContext<Self>) -> Self {
116        let (io_tx, mut io_rx) = mpsc::unbounded();
117        let this = Self {
118            projects: HashMap::default(),
119            io_tx,
120        };
121        cx.spawn_weak(|this, mut cx| async move {
122            while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
123                if let Some(this) = this.upgrade(&cx) {
124                    this.update(&mut cx, |this, cx| {
125                        message.push('\n');
126                        this.on_io(project, server_id, io_kind, &message, cx);
127                    });
128                }
129            }
130            anyhow::Ok(())
131        })
132        .detach();
133        this
134    }
135
136    pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
137        use project::Event::*;
138
139        let weak_project = project.downgrade();
140        self.projects.insert(
141            weak_project,
142            ProjectState {
143                servers: HashMap::default(),
144                _subscriptions: [
145                    cx.observe_release(&project, move |this, _, _| {
146                        this.projects.remove(&weak_project);
147                    }),
148                    cx.subscribe(project, |this, project, event, cx| match event {
149                        LanguageServerAdded(id) => {
150                            this.add_language_server(&project, *id, cx);
151                        }
152                        LanguageServerRemoved(id) => {
153                            this.remove_language_server(&project, *id, cx);
154                        }
155                        LanguageServerLog(id, message) => {
156                            this.add_language_server_log(&project, *id, message, cx);
157                        }
158                        _ => {}
159                    }),
160                ],
161            },
162        );
163    }
164
165    fn add_language_server(
166        &mut self,
167        project: &ModelHandle<Project>,
168        id: LanguageServerId,
169        cx: &mut ModelContext<Self>,
170    ) -> Option<ModelHandle<Buffer>> {
171        let project_state = self.projects.get_mut(&project.downgrade())?;
172        let server_state = project_state.servers.entry(id).or_insert_with(|| {
173            cx.notify();
174            LanguageServerState {
175                rpc_state: None,
176                log_buffer: cx
177                    .add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
178                    .clone(),
179                _subscription: None,
180            }
181        });
182
183        let server = project.read(cx).language_server_for_id(id);
184        let weak_project = project.downgrade();
185        let io_tx = self.io_tx.clone();
186        server_state._subscription = server.map(|server| {
187            server.on_io(move |io_kind, message| {
188                io_tx
189                    .unbounded_send((weak_project, id, io_kind, message.to_string()))
190                    .ok();
191            })
192        });
193
194        Some(server_state.log_buffer.clone())
195    }
196
197    fn add_language_server_log(
198        &mut self,
199        project: &ModelHandle<Project>,
200        id: LanguageServerId,
201        message: &str,
202        cx: &mut ModelContext<Self>,
203    ) -> Option<()> {
204        let buffer = self.add_language_server(&project, id, cx)?;
205        buffer.update(cx, |buffer, cx| {
206            let len = buffer.len();
207            let has_newline = message.ends_with("\n");
208            buffer.edit([(len..len, message)], None, cx);
209            if !has_newline {
210                let len = buffer.len();
211                buffer.edit([(len..len, "\n")], None, cx);
212            }
213        });
214        cx.notify();
215        Some(())
216    }
217
218    fn remove_language_server(
219        &mut self,
220        project: &ModelHandle<Project>,
221        id: LanguageServerId,
222        cx: &mut ModelContext<Self>,
223    ) -> Option<()> {
224        let project_state = self.projects.get_mut(&project.downgrade())?;
225        project_state.servers.remove(&id);
226        cx.notify();
227        Some(())
228    }
229
230    pub fn log_buffer_for_server(
231        &self,
232        project: &ModelHandle<Project>,
233        server_id: LanguageServerId,
234    ) -> Option<ModelHandle<Buffer>> {
235        let weak_project = project.downgrade();
236        let project_state = self.projects.get(&weak_project)?;
237        let server_state = project_state.servers.get(&server_id)?;
238        Some(server_state.log_buffer.clone())
239    }
240
241    fn enable_rpc_trace_for_language_server(
242        &mut self,
243        project: &ModelHandle<Project>,
244        server_id: LanguageServerId,
245        cx: &mut ModelContext<Self>,
246    ) -> Option<ModelHandle<Buffer>> {
247        let weak_project = project.downgrade();
248        let project_state = self.projects.get_mut(&weak_project)?;
249        let server_state = project_state.servers.get_mut(&server_id)?;
250        let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
251            let language = project.read(cx).languages().language_for_name("JSON");
252            let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
253            cx.spawn_weak({
254                let buffer = buffer.clone();
255                |_, mut cx| async move {
256                    let language = language.await.ok();
257                    buffer.update(&mut cx, |buffer, cx| {
258                        buffer.set_language(language, cx);
259                    });
260                }
261            })
262            .detach();
263
264            LanguageServerRpcState {
265                buffer,
266                last_message_kind: None,
267            }
268        });
269        Some(rpc_state.buffer.clone())
270    }
271
272    pub fn disable_rpc_trace_for_language_server(
273        &mut self,
274        project: &ModelHandle<Project>,
275        server_id: LanguageServerId,
276        _: &mut ModelContext<Self>,
277    ) -> Option<()> {
278        let project = project.downgrade();
279        let project_state = self.projects.get_mut(&project)?;
280        let server_state = project_state.servers.get_mut(&server_id)?;
281        server_state.rpc_state.take();
282        Some(())
283    }
284
285    fn on_io(
286        &mut self,
287        project: WeakModelHandle<Project>,
288        language_server_id: LanguageServerId,
289        io_kind: IoKind,
290        message: &str,
291        cx: &mut AppContext,
292    ) -> Option<()> {
293        let is_received = match io_kind {
294            IoKind::StdOut => true,
295            IoKind::StdIn => false,
296            IoKind::StdErr => {
297                let project = project.upgrade(cx)?;
298                project.update(cx, |_, cx| {
299                    cx.emit(project::Event::LanguageServerLog(
300                        language_server_id,
301                        format!("stderr: {}\n", message.trim()),
302                    ))
303                });
304                return Some(());
305            }
306        };
307
308        let state = self
309            .projects
310            .get_mut(&project)?
311            .servers
312            .get_mut(&language_server_id)?
313            .rpc_state
314            .as_mut()?;
315        state.buffer.update(cx, |buffer, cx| {
316            let kind = if is_received {
317                MessageKind::Receive
318            } else {
319                MessageKind::Send
320            };
321            if state.last_message_kind != Some(kind) {
322                let len = buffer.len();
323                let line = match kind {
324                    MessageKind::Send => SEND_LINE,
325                    MessageKind::Receive => RECEIVE_LINE,
326                };
327                buffer.edit([(len..len, line)], None, cx);
328                state.last_message_kind = Some(kind);
329            }
330            let len = buffer.len();
331            buffer.edit([(len..len, message)], None, cx);
332        });
333        Some(())
334    }
335}
336
337impl LspLogView {
338    pub fn new(
339        project: ModelHandle<Project>,
340        log_store: ModelHandle<LogStore>,
341        cx: &mut ViewContext<Self>,
342    ) -> Self {
343        let server_id = log_store
344            .read(cx)
345            .projects
346            .get(&project.downgrade())
347            .and_then(|project| project.servers.keys().copied().next());
348        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
349        let mut this = Self {
350            editor: Self::editor_for_buffer(project.clone(), buffer, cx),
351            project,
352            log_store,
353            current_server_id: None,
354            is_showing_rpc_trace: false,
355        };
356        if let Some(server_id) = server_id {
357            this.show_logs_for_server(server_id, cx);
358        }
359        this
360    }
361
362    fn editor_for_buffer(
363        project: ModelHandle<Project>,
364        buffer: ModelHandle<Buffer>,
365        cx: &mut ViewContext<Self>,
366    ) -> ViewHandle<Editor> {
367        let editor = cx.add_view(|cx| {
368            let mut editor = Editor::for_buffer(buffer, Some(project), cx);
369            editor.set_read_only(true);
370            editor.move_to_end(&Default::default(), cx);
371            editor
372        });
373        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
374            .detach();
375        editor
376    }
377
378    pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
379        let log_store = self.log_store.read(cx);
380        let state = log_store.projects.get(&self.project.downgrade())?;
381        let mut rows = self
382            .project
383            .read(cx)
384            .language_servers()
385            .filter_map(|(server_id, language_server_name, worktree_id)| {
386                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
387                let state = state.servers.get(&server_id)?;
388                Some(LogMenuItem {
389                    server_id,
390                    server_name: language_server_name,
391                    worktree,
392                    rpc_trace_enabled: state.rpc_state.is_some(),
393                    rpc_trace_selected: self.is_showing_rpc_trace
394                        && self.current_server_id == Some(server_id),
395                    logs_selected: !self.is_showing_rpc_trace
396                        && self.current_server_id == Some(server_id),
397                })
398            })
399            .collect::<Vec<_>>();
400        rows.sort_by_key(|row| row.server_id);
401        rows.dedup_by_key(|row| row.server_id);
402        Some(rows)
403    }
404
405    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
406        let buffer = self
407            .log_store
408            .read(cx)
409            .log_buffer_for_server(&self.project, server_id);
410        if let Some(buffer) = buffer {
411            self.current_server_id = Some(server_id);
412            self.is_showing_rpc_trace = false;
413            self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
414            cx.notify();
415        }
416    }
417
418    fn show_rpc_trace_for_server(
419        &mut self,
420        server_id: LanguageServerId,
421        cx: &mut ViewContext<Self>,
422    ) {
423        let buffer = self.log_store.update(cx, |log_set, cx| {
424            log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
425        });
426        if let Some(buffer) = buffer {
427            self.current_server_id = Some(server_id);
428            self.is_showing_rpc_trace = true;
429            self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
430            cx.notify();
431        }
432    }
433
434    fn toggle_rpc_trace_for_server(
435        &mut self,
436        server_id: LanguageServerId,
437        enabled: bool,
438        cx: &mut ViewContext<Self>,
439    ) {
440        self.log_store.update(cx, |log_store, cx| {
441            if enabled {
442                log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
443            } else {
444                log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
445            }
446        });
447        if !enabled && Some(server_id) == self.current_server_id {
448            self.show_logs_for_server(server_id, cx);
449            cx.notify();
450        }
451    }
452}
453
454impl View for LspLogView {
455    fn ui_name() -> &'static str {
456        "LspLogView"
457    }
458
459    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
460        ChildView::new(&self.editor, cx).into_any()
461    }
462
463    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
464        if cx.is_self_focused() {
465            cx.focus(&self.editor);
466        }
467    }
468}
469
470impl Item for LspLogView {
471    fn tab_content<V: 'static>(
472        &self,
473        _: Option<usize>,
474        style: &theme::Tab,
475        _: &AppContext,
476    ) -> AnyElement<V> {
477        Label::new("LSP Logs", style.label.clone()).into_any()
478    }
479
480    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
481        Some(Box::new(handle.clone()))
482    }
483}
484
485impl SearchableItem for LspLogView {
486    type Match = <Editor as SearchableItem>::Match;
487
488    fn to_search_event(
489        &mut self,
490        event: &Self::Event,
491        cx: &mut ViewContext<Self>,
492    ) -> Option<workspace::searchable::SearchEvent> {
493        self.editor
494            .update(cx, |editor, cx| editor.to_search_event(event, cx))
495    }
496
497    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
498        self.editor.update(cx, |e, cx| e.clear_matches(cx))
499    }
500
501    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
502        self.editor
503            .update(cx, |e, cx| e.update_matches(matches, cx))
504    }
505
506    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
507        self.editor.update(cx, |e, cx| e.query_suggestion(cx))
508    }
509
510    fn activate_match(
511        &mut self,
512        index: usize,
513        matches: Vec<Self::Match>,
514        cx: &mut ViewContext<Self>,
515    ) {
516        self.editor
517            .update(cx, |e, cx| e.activate_match(index, matches, cx))
518    }
519
520    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
521        self.editor
522            .update(cx, |e, cx| e.select_matches(matches, cx))
523    }
524
525    fn find_matches(
526        &mut self,
527        query: project::search::SearchQuery,
528        cx: &mut ViewContext<Self>,
529    ) -> gpui::Task<Vec<Self::Match>> {
530        self.editor.update(cx, |e, cx| e.find_matches(query, cx))
531    }
532
533    fn active_match_index(
534        &mut self,
535        matches: Vec<Self::Match>,
536        cx: &mut ViewContext<Self>,
537    ) -> Option<usize> {
538        self.editor
539            .update(cx, |e, cx| e.active_match_index(matches, cx))
540    }
541}
542
543impl ToolbarItemView for LspLogToolbarItemView {
544    fn set_active_pane_item(
545        &mut self,
546        active_pane_item: Option<&dyn ItemHandle>,
547        _: &mut ViewContext<Self>,
548    ) -> workspace::ToolbarItemLocation {
549        self.menu_open = false;
550        if let Some(item) = active_pane_item {
551            if let Some(log_view) = item.downcast::<LspLogView>() {
552                self.log_view = Some(log_view.clone());
553                return ToolbarItemLocation::PrimaryLeft {
554                    flex: Some((1., false)),
555                };
556            }
557        }
558        self.log_view = None;
559        ToolbarItemLocation::Hidden
560    }
561}
562
563impl View for LspLogToolbarItemView {
564    fn ui_name() -> &'static str {
565        "LspLogView"
566    }
567
568    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
569        let theme = theme::current(cx).clone();
570        let Some(log_view) = self.log_view.as_ref() else {
571            return Empty::new().into_any();
572        };
573        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
574            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
575            let current_server_id = log_view.current_server_id;
576            (menu_rows, current_server_id)
577        });
578
579        let current_server = current_server_id.and_then(|current_server_id| {
580            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
581                Some(menu_rows[ix].clone())
582            } else {
583                None
584            }
585        });
586        let server_selected = current_server.is_some();
587
588        enum Menu {}
589        let lsp_menu = Stack::new()
590            .with_child(Self::render_language_server_menu_header(
591                current_server,
592                &theme,
593                cx,
594            ))
595            .with_children(if self.menu_open {
596                Some(
597                    Overlay::new(
598                        MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
599                            Flex::column()
600                                .with_children(menu_rows.into_iter().map(|row| {
601                                    Self::render_language_server_menu_item(
602                                        row.server_id,
603                                        row.server_name,
604                                        row.worktree,
605                                        row.rpc_trace_enabled,
606                                        row.logs_selected,
607                                        row.rpc_trace_selected,
608                                        &theme,
609                                        cx,
610                                    )
611                                }))
612                                .contained()
613                                .with_style(theme.toolbar_dropdown_menu.container)
614                                .constrained()
615                                .with_width(400.)
616                                .with_height(400.)
617                        })
618                        .on_down_out(MouseButton::Left, |_, this, cx| {
619                            this.menu_open = false;
620                            cx.notify()
621                        }),
622                    )
623                    .with_hoverable(true)
624                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
625                    .with_anchor_corner(AnchorCorner::TopLeft)
626                    .with_z_index(999)
627                    .aligned()
628                    .bottom()
629                    .left(),
630                )
631            } else {
632                None
633            })
634            .aligned()
635            .left()
636            .clipped();
637
638        enum LspCleanupButton {}
639        let log_cleanup_button =
640            MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
641                let theme = theme::current(cx).clone();
642                let style = theme
643                    .workspace
644                    .toolbar
645                    .toggleable_text_tool
646                    .in_state(server_selected)
647                    .style_for(state);
648                Label::new("Clear", style.text.clone())
649                    .aligned()
650                    .contained()
651                    .with_style(style.container)
652                    .constrained()
653                    .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
654            })
655            .on_click(MouseButton::Left, move |_, this, cx| {
656                if let Some(log_view) = this.log_view.as_ref() {
657                    log_view.update(cx, |log_view, cx| {
658                        log_view.editor.update(cx, |editor, cx| {
659                            editor.set_read_only(false);
660                            editor.clear(cx);
661                            editor.set_read_only(true);
662                        });
663                    })
664                }
665            })
666            .with_cursor_style(CursorStyle::PointingHand)
667            .aligned()
668            .right();
669
670        Flex::row()
671            .with_child(lsp_menu)
672            .with_child(log_cleanup_button)
673            .contained()
674            .aligned()
675            .left()
676            .into_any_named("lsp log controls")
677    }
678}
679
680const RPC_MESSAGES: &str = "RPC Messages";
681const SERVER_LOGS: &str = "Server Logs";
682
683impl LspLogToolbarItemView {
684    pub fn new() -> Self {
685        Self {
686            menu_open: false,
687            log_view: None,
688        }
689    }
690
691    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
692        self.menu_open = !self.menu_open;
693        cx.notify();
694    }
695
696    fn toggle_logging_for_server(
697        &mut self,
698        id: LanguageServerId,
699        enabled: bool,
700        cx: &mut ViewContext<Self>,
701    ) {
702        if let Some(log_view) = &self.log_view {
703            log_view.update(cx, |log_view, cx| {
704                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
705                if !enabled && Some(id) == log_view.current_server_id {
706                    log_view.show_logs_for_server(id, cx);
707                    cx.notify();
708                }
709            });
710        }
711        cx.notify();
712    }
713
714    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
715        if let Some(log_view) = &self.log_view {
716            log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
717            self.menu_open = false;
718            cx.notify();
719        }
720    }
721
722    fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
723        if let Some(log_view) = &self.log_view {
724            log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
725            self.menu_open = false;
726            cx.notify();
727        }
728    }
729
730    fn render_language_server_menu_header(
731        current_server: Option<LogMenuItem>,
732        theme: &Arc<Theme>,
733        cx: &mut ViewContext<Self>,
734    ) -> impl Element<Self> {
735        enum ToggleMenu {}
736        MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, cx| {
737            let label: Cow<str> = current_server
738                .and_then(|row| {
739                    let worktree = row.worktree.read(cx);
740                    Some(
741                        format!(
742                            "{} ({}) - {}",
743                            row.server_name.0,
744                            worktree.root_name(),
745                            if row.rpc_trace_selected {
746                                RPC_MESSAGES
747                            } else {
748                                SERVER_LOGS
749                            },
750                        )
751                        .into(),
752                    )
753                })
754                .unwrap_or_else(|| "No server selected".into());
755            let style = theme.toolbar_dropdown_menu.header.style_for(state);
756            Label::new(label, style.text.clone())
757                .contained()
758                .with_style(style.container)
759        })
760        .with_cursor_style(CursorStyle::PointingHand)
761        .on_click(MouseButton::Left, move |_, view, cx| {
762            view.toggle_menu(cx);
763        })
764    }
765
766    fn render_language_server_menu_item(
767        id: LanguageServerId,
768        name: LanguageServerName,
769        worktree: ModelHandle<Worktree>,
770        rpc_trace_enabled: bool,
771        logs_selected: bool,
772        rpc_trace_selected: bool,
773        theme: &Arc<Theme>,
774        cx: &mut ViewContext<Self>,
775    ) -> impl Element<Self> {
776        enum ActivateLog {}
777        enum ActivateRpcTrace {}
778
779        Flex::column()
780            .with_child({
781                let style = &theme.toolbar_dropdown_menu.section_header;
782                Label::new(
783                    format!("{} ({})", name.0, worktree.read(cx).root_name()),
784                    style.text.clone(),
785                )
786                .contained()
787                .with_style(style.container)
788                .constrained()
789                .with_height(theme.toolbar_dropdown_menu.row_height)
790            })
791            .with_child(
792                MouseEventHandler::new::<ActivateLog, _>(id.0, cx, move |state, _| {
793                    let style = theme
794                        .toolbar_dropdown_menu
795                        .item
796                        .in_state(logs_selected)
797                        .style_for(state);
798                    Label::new(SERVER_LOGS, style.text.clone())
799                        .contained()
800                        .with_style(style.container)
801                        .constrained()
802                        .with_height(theme.toolbar_dropdown_menu.row_height)
803                })
804                .with_cursor_style(CursorStyle::PointingHand)
805                .on_click(MouseButton::Left, move |_, view, cx| {
806                    view.show_logs_for_server(id, cx);
807                }),
808            )
809            .with_child(
810                MouseEventHandler::new::<ActivateRpcTrace, _>(id.0, cx, move |state, cx| {
811                    let style = theme
812                        .toolbar_dropdown_menu
813                        .item
814                        .in_state(rpc_trace_selected)
815                        .style_for(state);
816                    Flex::row()
817                        .with_child(
818                            Label::new(RPC_MESSAGES, style.text.clone())
819                                .constrained()
820                                .with_height(theme.toolbar_dropdown_menu.row_height),
821                        )
822                        .with_child(
823                            ui::checkbox_with_label::<Self, _, Self, _>(
824                                Empty::new(),
825                                &theme.welcome.checkbox,
826                                rpc_trace_enabled,
827                                id.0,
828                                cx,
829                                move |this, enabled, cx| {
830                                    this.toggle_logging_for_server(id, enabled, cx);
831                                },
832                            )
833                            .flex_float(),
834                        )
835                        .align_children_center()
836                        .contained()
837                        .with_style(style.container)
838                        .constrained()
839                        .with_height(theme.toolbar_dropdown_menu.row_height)
840                })
841                .with_cursor_style(CursorStyle::PointingHand)
842                .on_click(MouseButton::Left, move |_, view, cx| {
843                    view.show_rpc_trace_for_server(id, cx);
844                }),
845            )
846    }
847}
848
849impl Entity for LogStore {
850    type Event = ();
851}
852
853impl Entity for LspLogView {
854    type Event = editor::Event;
855}
856
857impl Entity for LspLogToolbarItemView {
858    type Event = ();
859}