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