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