lsp_log.rs

  1use collections::{HashMap, VecDeque};
  2use editor::{Editor, EditorElement, EditorEvent, MoveToEnd};
  3use futures::{channel::mpsc, StreamExt};
  4use gpui::{
  5    actions, div, overlay, red, AnyElement, AppContext, Context, CursorStyle, Div, EventEmitter,
  6    FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, MouseButton,
  7    MouseDownEvent, ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext,
  8    WeakModel, WindowContext,
  9};
 10use language::{LanguageServerId, LanguageServerName};
 11use lsp::IoKind;
 12use project::{search::SearchQuery, Project};
 13use std::{borrow::Cow, sync::Arc};
 14use ui::{h_stack, v_stack, Checkbox, Label};
 15use workspace::{
 16    item::{Item, ItemHandle},
 17    searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
 18    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 19};
 20
 21const SEND_LINE: &str = "// Send:";
 22const RECEIVE_LINE: &str = "// Receive:";
 23const MAX_STORED_LOG_ENTRIES: usize = 2000;
 24
 25pub struct LogStore {
 26    projects: HashMap<WeakModel<Project>, ProjectState>,
 27    io_tx: mpsc::UnboundedSender<(WeakModel<Project>, LanguageServerId, IoKind, String)>,
 28}
 29
 30struct ProjectState {
 31    servers: HashMap<LanguageServerId, LanguageServerState>,
 32    _subscriptions: [gpui::Subscription; 2],
 33}
 34
 35struct LanguageServerState {
 36    log_messages: VecDeque<String>,
 37    rpc_state: Option<LanguageServerRpcState>,
 38    _io_logs_subscription: Option<lsp::Subscription>,
 39    _lsp_logs_subscription: Option<lsp::Subscription>,
 40}
 41
 42struct LanguageServerRpcState {
 43    rpc_messages: VecDeque<String>,
 44    last_message_kind: Option<MessageKind>,
 45}
 46
 47pub struct LspLogView {
 48    pub(crate) editor: View<Editor>,
 49    editor_subscription: Subscription,
 50    log_store: Model<LogStore>,
 51    current_server_id: Option<LanguageServerId>,
 52    is_showing_rpc_trace: bool,
 53    project: Model<Project>,
 54    focus_handle: FocusHandle,
 55    _log_store_subscriptions: Vec<Subscription>,
 56}
 57
 58pub struct LspLogToolbarItemView {
 59    log_view: Option<View<LspLogView>>,
 60    _log_view_subscription: Option<Subscription>,
 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)]
 71pub(crate) struct LogMenuItem {
 72    pub server_id: LanguageServerId,
 73    pub server_name: LanguageServerName,
 74    pub worktree_root_name: String,
 75    pub rpc_trace_enabled: bool,
 76    pub rpc_trace_selected: bool,
 77    pub logs_selected: bool,
 78}
 79
 80actions!(debug, [OpenLanguageServerLogs]);
 81
 82pub fn init(cx: &mut AppContext) {
 83    let log_store = cx.build_model(|cx| LogStore::new(cx));
 84
 85    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
 86        let project = workspace.project();
 87        if project.read(cx).is_local() {
 88            log_store.update(cx, |store, cx| {
 89                store.add_project(&project, cx);
 90            });
 91        }
 92
 93        let log_store = log_store.clone();
 94        workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
 95            let project = workspace.project().read(cx);
 96            if project.is_local() {
 97                workspace.add_item(
 98                    Box::new(cx.build_view(|cx| {
 99                        LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
100                    })),
101                    cx,
102                );
103            }
104        });
105    })
106    .detach();
107}
108
109impl LogStore {
110    pub fn new(cx: &mut ModelContext<Self>) -> Self {
111        let (io_tx, mut io_rx) = mpsc::unbounded();
112        let this = Self {
113            projects: HashMap::default(),
114            io_tx,
115        };
116        cx.spawn(|this, mut cx| async move {
117            while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
118                if let Some(this) = this.upgrade() {
119                    this.update(&mut cx, |this, cx| {
120                        this.on_io(project, server_id, io_kind, &message, cx);
121                    })?;
122                }
123            }
124            anyhow::Ok(())
125        })
126        .detach_and_log_err(cx);
127        this
128    }
129
130    pub fn add_project(&mut self, project: &Model<Project>, cx: &mut ModelContext<Self>) {
131        let weak_project = project.downgrade();
132        self.projects.insert(
133            project.downgrade(),
134            ProjectState {
135                servers: HashMap::default(),
136                _subscriptions: [
137                    cx.observe_release(project, move |this, _, _| {
138                        this.projects.remove(&weak_project);
139                    }),
140                    cx.subscribe(project, |this, project, event, cx| match event {
141                        project::Event::LanguageServerAdded(id) => {
142                            this.add_language_server(&project, *id, cx);
143                        }
144                        project::Event::LanguageServerRemoved(id) => {
145                            this.remove_language_server(&project, *id, cx);
146                        }
147                        project::Event::LanguageServerLog(id, message) => {
148                            this.add_language_server_log(&project, *id, message, cx);
149                        }
150                        _ => {}
151                    }),
152                ],
153            },
154        );
155    }
156
157    fn add_language_server(
158        &mut self,
159        project: &Model<Project>,
160        id: LanguageServerId,
161        cx: &mut ModelContext<Self>,
162    ) -> Option<&mut LanguageServerState> {
163        let project_state = self.projects.get_mut(&project.downgrade())?;
164        let server_state = project_state.servers.entry(id).or_insert_with(|| {
165            cx.notify();
166            LanguageServerState {
167                rpc_state: None,
168                log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
169                _io_logs_subscription: None,
170                _lsp_logs_subscription: None,
171            }
172        });
173
174        let server = project.read(cx).language_server_for_id(id);
175        if let Some(server) = server.as_deref() {
176            if server.has_notification_handler::<lsp::notification::LogMessage>() {
177                // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
178                return Some(server_state);
179            }
180        }
181
182        let weak_project = project.downgrade();
183        let io_tx = self.io_tx.clone();
184        server_state._io_logs_subscription = server.as_ref().map(|server| {
185            server.on_io(move |io_kind, message| {
186                io_tx
187                    .unbounded_send((weak_project.clone(), id, io_kind, message.to_string()))
188                    .ok();
189            })
190        });
191        let this = cx.handle().downgrade();
192        let weak_project = project.downgrade();
193        server_state._lsp_logs_subscription = server.map(|server| {
194            let server_id = server.server_id();
195            server.on_notification::<lsp::notification::LogMessage, _>({
196                move |params, mut cx| {
197                    if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) {
198                        this.update(&mut cx, |this, cx| {
199                            this.add_language_server_log(&project, server_id, &params.message, cx);
200                        })
201                        .ok();
202                    }
203                }
204            })
205        });
206        Some(server_state)
207    }
208
209    fn add_language_server_log(
210        &mut self,
211        project: &Model<Project>,
212        id: LanguageServerId,
213        message: &str,
214        cx: &mut ModelContext<Self>,
215    ) -> Option<()> {
216        let language_server_state = match self
217            .projects
218            .get_mut(&project.downgrade())?
219            .servers
220            .get_mut(&id)
221        {
222            Some(existing_state) => existing_state,
223            None => self.add_language_server(&project, id, cx)?,
224        };
225
226        let log_lines = &mut language_server_state.log_messages;
227        while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
228            log_lines.pop_front();
229        }
230        let message = message.trim();
231        log_lines.push_back(message.to_string());
232        cx.emit(Event::NewServerLogEntry {
233            id,
234            entry: message.to_string(),
235            is_rpc: false,
236        });
237        cx.notify();
238        Some(())
239    }
240
241    fn remove_language_server(
242        &mut self,
243        project: &Model<Project>,
244        id: LanguageServerId,
245        cx: &mut ModelContext<Self>,
246    ) -> Option<()> {
247        let project_state = self.projects.get_mut(&project.downgrade())?;
248        project_state.servers.remove(&id);
249        cx.notify();
250        Some(())
251    }
252
253    fn server_logs(
254        &self,
255        project: &Model<Project>,
256        server_id: LanguageServerId,
257    ) -> Option<&VecDeque<String>> {
258        let weak_project = project.downgrade();
259        let project_state = self.projects.get(&weak_project)?;
260        let server_state = project_state.servers.get(&server_id)?;
261        Some(&server_state.log_messages)
262    }
263
264    fn enable_rpc_trace_for_language_server(
265        &mut self,
266        project: &Model<Project>,
267        server_id: LanguageServerId,
268    ) -> Option<&mut LanguageServerRpcState> {
269        let weak_project = project.downgrade();
270        let project_state = self.projects.get_mut(&weak_project)?;
271        let server_state = project_state.servers.get_mut(&server_id)?;
272        let rpc_state = server_state
273            .rpc_state
274            .get_or_insert_with(|| LanguageServerRpcState {
275                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
276                last_message_kind: None,
277            });
278        Some(rpc_state)
279    }
280
281    pub fn disable_rpc_trace_for_language_server(
282        &mut self,
283        project: &Model<Project>,
284        server_id: LanguageServerId,
285        _: &mut ModelContext<Self>,
286    ) -> Option<()> {
287        let project = project.downgrade();
288        let project_state = self.projects.get_mut(&project)?;
289        let server_state = project_state.servers.get_mut(&server_id)?;
290        server_state.rpc_state.take();
291        Some(())
292    }
293
294    fn on_io(
295        &mut self,
296        project: WeakModel<Project>,
297        language_server_id: LanguageServerId,
298        io_kind: IoKind,
299        message: &str,
300        cx: &mut ModelContext<Self>,
301    ) -> Option<()> {
302        let is_received = match io_kind {
303            IoKind::StdOut => true,
304            IoKind::StdIn => false,
305            IoKind::StdErr => {
306                let project = project.upgrade()?;
307                let message = format!("stderr: {}", message.trim());
308                self.add_language_server_log(&project, language_server_id, &message, cx);
309                return Some(());
310            }
311        };
312
313        let state = self
314            .projects
315            .get_mut(&project)?
316            .servers
317            .get_mut(&language_server_id)?
318            .rpc_state
319            .as_mut()?;
320        let kind = if is_received {
321            MessageKind::Receive
322        } else {
323            MessageKind::Send
324        };
325
326        let rpc_log_lines = &mut state.rpc_messages;
327        if state.last_message_kind != Some(kind) {
328            let line_before_message = match kind {
329                MessageKind::Send => SEND_LINE,
330                MessageKind::Receive => RECEIVE_LINE,
331            };
332            rpc_log_lines.push_back(line_before_message.to_string());
333            cx.emit(Event::NewServerLogEntry {
334                id: language_server_id,
335                entry: line_before_message.to_string(),
336                is_rpc: true,
337            });
338        }
339
340        while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
341            rpc_log_lines.pop_front();
342        }
343        let message = message.trim();
344        rpc_log_lines.push_back(message.to_string());
345        cx.emit(Event::NewServerLogEntry {
346            id: language_server_id,
347            entry: message.to_string(),
348            is_rpc: true,
349        });
350        cx.notify();
351        Some(())
352    }
353}
354
355impl LspLogView {
356    pub fn new(
357        project: Model<Project>,
358        log_store: Model<LogStore>,
359        cx: &mut ViewContext<Self>,
360    ) -> Self {
361        let server_id = log_store
362            .read(cx)
363            .projects
364            .get(&project.downgrade())
365            .and_then(|project| project.servers.keys().copied().next());
366        let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
367            (|| -> Option<()> {
368                let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
369                if let Some(current_lsp) = this.current_server_id {
370                    if !project_state.servers.contains_key(&current_lsp) {
371                        if let Some(server) = project_state.servers.iter().next() {
372                            if this.is_showing_rpc_trace {
373                                this.show_rpc_trace_for_server(*server.0, cx)
374                            } else {
375                                this.show_logs_for_server(*server.0, cx)
376                            }
377                        } else {
378                            this.current_server_id = None;
379                            this.editor.update(cx, |editor, cx| {
380                                editor.set_read_only(false);
381                                editor.clear(cx);
382                                editor.set_read_only(true);
383                            });
384                            cx.notify();
385                        }
386                    }
387                } else {
388                    if let Some(server) = project_state.servers.iter().next() {
389                        if this.is_showing_rpc_trace {
390                            this.show_rpc_trace_for_server(*server.0, cx)
391                        } else {
392                            this.show_logs_for_server(*server.0, cx)
393                        }
394                    }
395                }
396
397                Some(())
398            })();
399
400            cx.notify();
401        });
402        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
403            Event::NewServerLogEntry { id, entry, is_rpc } => {
404                if log_view.current_server_id == Some(*id) {
405                    if (*is_rpc && log_view.is_showing_rpc_trace)
406                        || (!*is_rpc && !log_view.is_showing_rpc_trace)
407                    {
408                        log_view.editor.update(cx, |editor, cx| {
409                            editor.set_read_only(false);
410                            editor.handle_input(entry.trim(), cx);
411                            editor.handle_input("\n", cx);
412                            editor.set_read_only(true);
413                        });
414                    }
415                }
416            }
417        });
418        let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
419
420        let focus_handle = cx.focus_handle();
421        let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
422            cx.focus_view(&log_view.editor);
423        });
424
425        let mut this = Self {
426            focus_handle,
427            editor,
428            editor_subscription,
429            project,
430            log_store,
431            current_server_id: None,
432            is_showing_rpc_trace: false,
433            _log_store_subscriptions: vec![
434                model_changes_subscription,
435                events_subscriptions,
436                focus_subscription,
437            ],
438        };
439        if let Some(server_id) = server_id {
440            this.show_logs_for_server(server_id, cx);
441        }
442        this
443    }
444
445    fn editor_for_logs(
446        log_contents: String,
447        cx: &mut ViewContext<Self>,
448    ) -> (View<Editor>, Subscription) {
449        let editor = cx.build_view(|cx| {
450            let mut editor = Editor::multi_line(cx);
451            editor.set_text(log_contents, cx);
452            editor.move_to_end(&MoveToEnd, cx);
453            editor.set_read_only(true);
454            editor
455        });
456        let editor_subscription = cx.subscribe(
457            &editor,
458            |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
459                cx.emit(event.clone())
460            },
461        );
462        (editor, editor_subscription)
463    }
464
465    pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
466        let log_store = self.log_store.read(cx);
467        let state = log_store.projects.get(&self.project.downgrade())?;
468        let mut rows = self
469            .project
470            .read(cx)
471            .language_servers()
472            .filter_map(|(server_id, language_server_name, worktree_id)| {
473                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
474                let state = state.servers.get(&server_id)?;
475                Some(LogMenuItem {
476                    server_id,
477                    server_name: language_server_name,
478                    worktree_root_name: worktree.read(cx).root_name().to_string(),
479                    rpc_trace_enabled: state.rpc_state.is_some(),
480                    rpc_trace_selected: self.is_showing_rpc_trace
481                        && self.current_server_id == Some(server_id),
482                    logs_selected: !self.is_showing_rpc_trace
483                        && self.current_server_id == Some(server_id),
484                })
485            })
486            .chain(
487                self.project
488                    .read(cx)
489                    .supplementary_language_servers()
490                    .filter_map(|(&server_id, (name, _))| {
491                        let state = state.servers.get(&server_id)?;
492                        Some(LogMenuItem {
493                            server_id,
494                            server_name: name.clone(),
495                            worktree_root_name: "supplementary".to_string(),
496                            rpc_trace_enabled: state.rpc_state.is_some(),
497                            rpc_trace_selected: self.is_showing_rpc_trace
498                                && self.current_server_id == Some(server_id),
499                            logs_selected: !self.is_showing_rpc_trace
500                                && self.current_server_id == Some(server_id),
501                        })
502                    }),
503            )
504            .collect::<Vec<_>>();
505        rows.sort_by_key(|row| row.server_id);
506        rows.dedup_by_key(|row| row.server_id);
507        Some(rows)
508    }
509
510    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
511        let log_contents = self
512            .log_store
513            .read(cx)
514            .server_logs(&self.project, server_id)
515            .map(log_contents);
516        if let Some(log_contents) = log_contents {
517            self.current_server_id = Some(server_id);
518            self.is_showing_rpc_trace = false;
519            let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
520            self.editor = editor;
521            self.editor_subscription = editor_subscription;
522            cx.notify();
523        }
524    }
525
526    fn show_rpc_trace_for_server(
527        &mut self,
528        server_id: LanguageServerId,
529        cx: &mut ViewContext<Self>,
530    ) {
531        let rpc_log = self.log_store.update(cx, |log_store, _| {
532            log_store
533                .enable_rpc_trace_for_language_server(&self.project, server_id)
534                .map(|state| log_contents(&state.rpc_messages))
535        });
536        if let Some(rpc_log) = rpc_log {
537            self.current_server_id = Some(server_id);
538            self.is_showing_rpc_trace = true;
539            let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
540            let language = self.project.read(cx).languages().language_for_name("JSON");
541            editor
542                .read(cx)
543                .buffer()
544                .read(cx)
545                .as_singleton()
546                .expect("log buffer should be a singleton")
547                .update(cx, |_, cx| {
548                    cx.spawn({
549                        let buffer = cx.handle();
550                        |_, mut cx| async move {
551                            let language = language.await.ok();
552                            buffer.update(&mut cx, |buffer, cx| {
553                                buffer.set_language(language, cx);
554                            })
555                        }
556                    })
557                    .detach_and_log_err(cx);
558                });
559
560            self.editor = editor;
561            self.editor_subscription = editor_subscription;
562            cx.notify();
563        }
564    }
565
566    fn toggle_rpc_trace_for_server(
567        &mut self,
568        server_id: LanguageServerId,
569        enabled: bool,
570        cx: &mut ViewContext<Self>,
571    ) {
572        self.log_store.update(cx, |log_store, cx| {
573            if enabled {
574                log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
575            } else {
576                log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
577            }
578        });
579        if !enabled && Some(server_id) == self.current_server_id {
580            self.show_logs_for_server(server_id, cx);
581            cx.notify();
582        }
583    }
584}
585
586fn log_contents(lines: &VecDeque<String>) -> String {
587    let (a, b) = lines.as_slices();
588    let log_contents = a.join("\n");
589    if b.is_empty() {
590        log_contents
591    } else {
592        log_contents + "\n" + &b.join("\n")
593    }
594}
595
596impl Render for LspLogView {
597    // todo!()
598    // fn ui_name() -> &'static str {
599    //     "LspLogView"
600    // }
601
602    type Element = EditorElement;
603
604    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
605        self.editor.update(cx, |editor, cx| editor.render(cx))
606    }
607}
608
609impl FocusableView for LspLogView {
610    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
611        self.focus_handle.clone()
612    }
613}
614
615impl Item for LspLogView {
616    type Event = EditorEvent;
617
618    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
619        Editor::to_item_events(event, f)
620    }
621
622    fn tab_content(&self, _: Option<usize>, _: bool, _: &WindowContext<'_>) -> AnyElement {
623        Label::new("LSP Logs").into_any_element()
624    }
625
626    fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
627        Some(Box::new(handle.clone()))
628    }
629}
630
631impl SearchableItem for LspLogView {
632    type Match = <Editor as SearchableItem>::Match;
633
634    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
635        self.editor.update(cx, |e, cx| e.clear_matches(cx))
636    }
637
638    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
639        self.editor
640            .update(cx, |e, cx| e.update_matches(matches, cx))
641    }
642
643    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
644        self.editor.update(cx, |e, cx| e.query_suggestion(cx))
645    }
646
647    fn activate_match(
648        &mut self,
649        index: usize,
650        matches: Vec<Self::Match>,
651        cx: &mut ViewContext<Self>,
652    ) {
653        self.editor
654            .update(cx, |e, cx| e.activate_match(index, matches, cx))
655    }
656
657    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
658        self.editor
659            .update(cx, |e, cx| e.select_matches(matches, cx))
660    }
661
662    fn find_matches(
663        &mut self,
664        query: Arc<project::search::SearchQuery>,
665        cx: &mut ViewContext<Self>,
666    ) -> gpui::Task<Vec<Self::Match>> {
667        self.editor.update(cx, |e, cx| e.find_matches(query, cx))
668    }
669
670    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
671        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
672    }
673    fn supported_options() -> workspace::searchable::SearchOptions {
674        workspace::searchable::SearchOptions {
675            case: true,
676            word: true,
677            regex: true,
678            // LSP log is read-only.
679            replacement: false,
680        }
681    }
682    fn active_match_index(
683        &mut self,
684        matches: Vec<Self::Match>,
685        cx: &mut ViewContext<Self>,
686    ) -> Option<usize> {
687        self.editor
688            .update(cx, |e, cx| e.active_match_index(matches, cx))
689    }
690}
691
692impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
693
694impl ToolbarItemView for LspLogToolbarItemView {
695    fn set_active_pane_item(
696        &mut self,
697        active_pane_item: Option<&dyn ItemHandle>,
698        cx: &mut ViewContext<Self>,
699    ) -> workspace::ToolbarItemLocation {
700        self.menu_open = false;
701        if let Some(item) = active_pane_item {
702            if let Some(log_view) = item.downcast::<LspLogView>() {
703                self.log_view = Some(log_view.clone());
704                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
705                    cx.notify();
706                }));
707                return ToolbarItemLocation::PrimaryLeft;
708            }
709        }
710        self.log_view = None;
711        self._log_view_subscription = None;
712        ToolbarItemLocation::Hidden
713    }
714}
715
716impl Render for LspLogToolbarItemView {
717    type Element = Div;
718    // todo!()
719    // fn ui_name() -> &'static str {
720    //     "LspLogView"
721    // }
722
723    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
724        let Some(log_view) = self.log_view.as_ref() else {
725            return div();
726        };
727        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
728            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
729            let current_server_id = log_view.current_server_id;
730            (menu_rows, current_server_id)
731        });
732
733        let current_server = current_server_id.and_then(|current_server_id| {
734            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
735                Some(menu_rows[ix].clone())
736            } else {
737                None
738            }
739        });
740        // todo!() styling
741        let _server_selected = current_server.is_some();
742
743        let lsp_menu = h_stack()
744            .child(Self::render_language_server_menu_header(current_server, cx))
745            .children(if self.menu_open {
746                Some(
747                    overlay().child(
748                        v_stack()
749                            // todo!()
750                            // .scrollable::<LspLogScroll>(0, None, cx)
751                            .children(menu_rows.into_iter().map(|row| {
752                                Self::render_language_server_menu_item(
753                                    row.server_id,
754                                    row.server_name,
755                                    &row.worktree_root_name,
756                                    row.rpc_trace_enabled,
757                                    row.logs_selected,
758                                    row.rpc_trace_selected,
759                                    cx,
760                                )
761                            }))
762                            .on_mouse_down_out(cx.listener(|this, event: &MouseDownEvent, cx| {
763                                if event.button == MouseButton::Left {
764                                    this.menu_open = false;
765                                    cx.notify()
766                                }
767                            })),
768                    ), // todo!()
769                       // .with_hoverable(true)
770                       // .with_fit_mode(OverlayFitMode::SwitchAnchor)
771                       // .with_anchor_corner(AnchorCorner::TopLeft)
772                       // .with_z_index(999),
773                )
774            } else {
775                None
776            });
777
778        let log_cleanup_button = div()
779            .child(Label::new("Clear"))
780            .on_mouse_down(
781                MouseButton::Left,
782                cx.listener(move |this, _, cx| {
783                    if let Some(log_view) = this.log_view.as_ref() {
784                        log_view.update(cx, |log_view, cx| {
785                            log_view.editor.update(cx, |editor, cx| {
786                                editor.set_read_only(false);
787                                editor.clear(cx);
788                                editor.set_read_only(true);
789                            });
790                        })
791                    }
792                }),
793            )
794            .cursor(CursorStyle::PointingHand);
795
796        h_stack()
797            .child(lsp_menu)
798            .child(log_cleanup_button)
799            .border_1()
800            .border_color(red())
801    }
802}
803
804const RPC_MESSAGES: &str = "RPC Messages";
805const SERVER_LOGS: &str = "Server Logs";
806
807impl LspLogToolbarItemView {
808    pub fn new() -> Self {
809        Self {
810            menu_open: false,
811            log_view: None,
812            _log_view_subscription: None,
813        }
814    }
815
816    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
817        self.menu_open = !self.menu_open;
818        cx.notify();
819    }
820
821    fn toggle_logging_for_server(
822        &mut self,
823        id: LanguageServerId,
824        enabled: bool,
825        cx: &mut ViewContext<Self>,
826    ) {
827        if let Some(log_view) = &self.log_view {
828            log_view.update(cx, |log_view, cx| {
829                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
830                if !enabled && Some(id) == log_view.current_server_id {
831                    log_view.show_logs_for_server(id, cx);
832                    cx.notify();
833                }
834            });
835        }
836        cx.notify();
837    }
838
839    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
840        if let Some(log_view) = &self.log_view {
841            log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
842            self.menu_open = false;
843            cx.notify();
844        }
845    }
846
847    fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
848        if let Some(log_view) = &self.log_view {
849            log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
850            self.menu_open = false;
851            cx.notify();
852        }
853    }
854
855    fn render_language_server_menu_header(
856        current_server: Option<LogMenuItem>,
857        cx: &mut ViewContext<Self>,
858    ) -> Div {
859        let label: Cow<str> = current_server
860            .and_then(|row| {
861                Some(
862                    format!(
863                        "{} ({}) - {}",
864                        row.server_name.0,
865                        row.worktree_root_name,
866                        if row.rpc_trace_selected {
867                            RPC_MESSAGES
868                        } else {
869                            SERVER_LOGS
870                        },
871                    )
872                    .into(),
873                )
874            })
875            .unwrap_or_else(|| "No server selected".into());
876        div()
877            .child(Label::new(label))
878            .cursor(CursorStyle::PointingHand)
879            .on_mouse_down(
880                MouseButton::Left,
881                cx.listener(move |view, _, cx| {
882                    view.toggle_menu(cx);
883                }),
884            )
885            .border_1()
886            .border_color(red())
887    }
888
889    fn render_language_server_menu_item(
890        id: LanguageServerId,
891        name: LanguageServerName,
892        worktree_root_name: &str,
893        rpc_trace_enabled: bool,
894        // todo!() styling
895        _logs_selected: bool,
896        _rpc_trace_selected: bool,
897        cx: &mut ViewContext<Self>,
898    ) -> Div {
899        v_stack()
900            .child(Label::new(format!("{} ({})", name.0, worktree_root_name)))
901            .child(
902                div()
903                    .child(Label::new(SERVER_LOGS))
904                    .cursor(CursorStyle::PointingHand)
905                    .on_mouse_down(
906                        MouseButton::Left,
907                        cx.listener(move |view, _, cx| {
908                            view.show_logs_for_server(id, cx);
909                        }),
910                    ),
911            )
912            .child(
913                h_stack()
914                    .child(Label::new(RPC_MESSAGES))
915                    .child(
916                        Checkbox::new(
917                            id.0,
918                            if rpc_trace_enabled {
919                                ui::Selection::Selected
920                            } else {
921                                ui::Selection::Unselected
922                            },
923                        )
924                        .on_click(cx.listener(
925                            move |this, selection, cx| {
926                                let enabled = matches!(selection, ui::Selection::Selected);
927                                this.toggle_logging_for_server(id, enabled, cx);
928                            },
929                        )),
930                    )
931                    .border_1()
932                    .border_color(red())
933                    .cursor(CursorStyle::PointingHand)
934                    .on_mouse_down(
935                        MouseButton::Left,
936                        cx.listener(move |view, _, cx| {
937                            view.show_rpc_trace_for_server(id, cx);
938                        }),
939                    ),
940            )
941            .border_1()
942            .border_color(red())
943    }
944}
945
946pub enum Event {
947    NewServerLogEntry {
948        id: LanguageServerId,
949        entry: String,
950        is_rpc: bool,
951    },
952}
953
954impl EventEmitter<Event> for LogStore {}
955impl EventEmitter<Event> for LspLogView {}
956impl EventEmitter<EditorEvent> for LspLogView {}
957impl EventEmitter<SearchEvent> for LspLogView {}