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