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