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