lsp_log.rs

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