lsp_log.rs

  1use collections::{HashMap, VecDeque};
  2use copilot::Copilot;
  3use editor::{actions::MoveToEnd, Editor, EditorEvent};
  4use futures::{channel::mpsc, StreamExt};
  5use gpui::{
  6    actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle,
  7    FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription,
  8    View, ViewContext, VisualContext, WeakModel, WindowContext,
  9};
 10use language::{LanguageServerId, LanguageServerName};
 11use lsp::{IoKind, LanguageServer};
 12use project::{search::SearchQuery, Project};
 13use std::{borrow::Cow, sync::Arc};
 14use ui::{popover_menu, prelude::*, Button, Checkbox, ContextMenu, Label, Selection};
 15use workspace::{
 16    item::{Item, ItemHandle, TabContentParams},
 17    searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
 18    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 19};
 20
 21const SEND_LINE: &str = "// Send:";
 22const RECEIVE_LINE: &str = "// Receive:";
 23const MAX_STORED_LOG_ENTRIES: usize = 2000;
 24
 25pub struct LogStore {
 26    projects: HashMap<WeakModel<Project>, ProjectState>,
 27    language_servers: HashMap<LanguageServerId, LanguageServerState>,
 28    copilot_log_subscription: Option<lsp::Subscription>,
 29    _copilot_subscription: Option<gpui::Subscription>,
 30    io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
 31}
 32
 33struct ProjectState {
 34    _subscriptions: [gpui::Subscription; 2],
 35}
 36
 37struct LanguageServerState {
 38    name: LanguageServerName,
 39    log_messages: VecDeque<String>,
 40    rpc_state: Option<LanguageServerRpcState>,
 41    project: Option<WeakModel<Project>>,
 42    _io_logs_subscription: Option<lsp::Subscription>,
 43    _lsp_logs_subscription: Option<lsp::Subscription>,
 44}
 45
 46struct LanguageServerRpcState {
 47    rpc_messages: VecDeque<String>,
 48    last_message_kind: Option<MessageKind>,
 49}
 50
 51pub struct LspLogView {
 52    pub(crate) editor: View<Editor>,
 53    editor_subscriptions: Vec<Subscription>,
 54    log_store: Model<LogStore>,
 55    current_server_id: Option<LanguageServerId>,
 56    is_showing_rpc_trace: bool,
 57    project: Model<Project>,
 58    focus_handle: FocusHandle,
 59    _log_store_subscriptions: Vec<Subscription>,
 60}
 61
 62pub struct LspLogToolbarItemView {
 63    log_view: Option<View<LspLogView>>,
 64    _log_view_subscription: Option<Subscription>,
 65}
 66
 67#[derive(Copy, Clone, PartialEq, Eq)]
 68enum MessageKind {
 69    Send,
 70    Receive,
 71}
 72
 73#[derive(Clone, Debug, PartialEq)]
 74pub(crate) struct LogMenuItem {
 75    pub server_id: LanguageServerId,
 76    pub server_name: LanguageServerName,
 77    pub worktree_root_name: String,
 78    pub rpc_trace_enabled: bool,
 79    pub rpc_trace_selected: bool,
 80    pub logs_selected: bool,
 81}
 82
 83actions!(debug, [OpenLanguageServerLogs]);
 84
 85pub fn init(cx: &mut AppContext) {
 86    let log_store = cx.new_model(|cx| LogStore::new(cx));
 87
 88    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
 89        let project = workspace.project();
 90        if project.read(cx).is_local() {
 91            log_store.update(cx, |store, cx| {
 92                store.add_project(&project, cx);
 93            });
 94        }
 95
 96        let log_store = log_store.clone();
 97        workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
 98            let project = workspace.project().read(cx);
 99            if project.is_local() {
100                workspace.add_item_to_active_pane(
101                    Box::new(cx.new_view(|cx| {
102                        LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
103                    })),
104                    None,
105                    cx,
106                );
107            }
108        });
109    })
110    .detach();
111}
112
113impl LogStore {
114    pub fn new(cx: &mut ModelContext<Self>) -> Self {
115        let (io_tx, mut io_rx) = mpsc::unbounded();
116
117        let copilot_subscription = Copilot::global(cx).map(|copilot| {
118            let copilot = &copilot;
119            cx.subscribe(
120                copilot,
121                |this, copilot, copilot_event, cx| match copilot_event {
122                    copilot::Event::CopilotLanguageServerStarted => {
123                        if let Some(server) = copilot.read(cx).language_server() {
124                            let server_id = server.server_id();
125                            let weak_this = cx.weak_model();
126                            this.copilot_log_subscription =
127                                Some(server.on_notification::<copilot::request::LogMessage, _>(
128                                    move |params, mut cx| {
129                                        weak_this
130                                            .update(&mut cx, |this, cx| {
131                                                this.add_language_server_log(
132                                                    server_id,
133                                                    &params.message,
134                                                    cx,
135                                                );
136                                            })
137                                            .ok();
138                                    },
139                                ));
140                            this.add_language_server(
141                                None,
142                                LanguageServerName(Arc::from("copilot")),
143                                server.clone(),
144                                cx,
145                            );
146                        }
147                    }
148                },
149            )
150        });
151
152        let this = Self {
153            copilot_log_subscription: None,
154            _copilot_subscription: copilot_subscription,
155            projects: HashMap::default(),
156            language_servers: HashMap::default(),
157            io_tx,
158        };
159
160        cx.spawn(|this, mut cx| async move {
161            while let Some((server_id, io_kind, message)) = io_rx.next().await {
162                if let Some(this) = this.upgrade() {
163                    this.update(&mut cx, |this, cx| {
164                        this.on_io(server_id, io_kind, &message, cx);
165                    })?;
166                }
167            }
168            anyhow::Ok(())
169        })
170        .detach_and_log_err(cx);
171        this
172    }
173
174    pub fn add_project(&mut self, project: &Model<Project>, cx: &mut ModelContext<Self>) {
175        let weak_project = project.downgrade();
176        self.projects.insert(
177            project.downgrade(),
178            ProjectState {
179                _subscriptions: [
180                    cx.observe_release(project, move |this, _, _| {
181                        this.projects.remove(&weak_project);
182                        this.language_servers
183                            .retain(|_, state| state.project.as_ref() != Some(&weak_project));
184                    }),
185                    cx.subscribe(project, |this, project, event, cx| match event {
186                        project::Event::LanguageServerAdded(id) => {
187                            let read_project = project.read(cx);
188                            if let Some((server, adapter)) = read_project
189                                .language_server_for_id(*id)
190                                .zip(read_project.language_server_adapter_for_id(*id))
191                            {
192                                this.add_language_server(
193                                    Some(&project.downgrade()),
194                                    adapter.name.clone(),
195                                    server,
196                                    cx,
197                                );
198                            }
199                        }
200                        project::Event::LanguageServerRemoved(id) => {
201                            this.remove_language_server(*id, cx);
202                        }
203                        project::Event::LanguageServerLog(id, message) => {
204                            this.add_language_server_log(*id, message, cx);
205                        }
206                        _ => {}
207                    }),
208                ],
209            },
210        );
211    }
212
213    fn get_language_server_state(
214        &mut self,
215        id: LanguageServerId,
216    ) -> Option<&mut LanguageServerState> {
217        self.language_servers.get_mut(&id)
218    }
219
220    fn add_language_server(
221        &mut self,
222        project: Option<&WeakModel<Project>>,
223        name: LanguageServerName,
224        server: Arc<LanguageServer>,
225        cx: &mut ModelContext<Self>,
226    ) -> Option<&mut LanguageServerState> {
227        let server_state = self
228            .language_servers
229            .entry(server.server_id())
230            .or_insert_with(|| {
231                cx.notify();
232                LanguageServerState {
233                    name,
234                    rpc_state: None,
235                    project: project.cloned(),
236                    log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
237                    _io_logs_subscription: None,
238                    _lsp_logs_subscription: None,
239                }
240            });
241
242        if server.has_notification_handler::<lsp::notification::LogMessage>() {
243            // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
244            return Some(server_state);
245        }
246
247        let io_tx = self.io_tx.clone();
248        let server_id = server.server_id();
249        server_state._io_logs_subscription = Some(server.on_io(move |io_kind, message| {
250            io_tx
251                .unbounded_send((server_id, io_kind, message.to_string()))
252                .ok();
253        }));
254        let this = cx.handle().downgrade();
255        server_state._lsp_logs_subscription =
256            Some(server.on_notification::<lsp::notification::LogMessage, _>({
257                move |params, mut cx| {
258                    if let Some(this) = this.upgrade() {
259                        this.update(&mut cx, |this, cx| {
260                            this.add_language_server_log(server_id, &params.message, cx);
261                        })
262                        .ok();
263                    }
264                }
265            }));
266        Some(server_state)
267    }
268
269    fn add_language_server_log(
270        &mut self,
271        id: LanguageServerId,
272        message: &str,
273        cx: &mut ModelContext<Self>,
274    ) -> Option<()> {
275        let language_server_state = self.get_language_server_state(id)?;
276
277        let log_lines = &mut language_server_state.log_messages;
278        while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
279            log_lines.pop_front();
280        }
281        let message = message.trim();
282        log_lines.push_back(message.to_string());
283        cx.emit(Event::NewServerLogEntry {
284            id,
285            entry: message.to_string(),
286            is_rpc: false,
287        });
288        cx.notify();
289        Some(())
290    }
291
292    fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext<Self>) {
293        self.language_servers.remove(&id);
294        cx.notify();
295    }
296
297    fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> {
298        Some(&self.language_servers.get(&server_id)?.log_messages)
299    }
300
301    fn server_ids_for_project<'a>(
302        &'a self,
303        project: &'a WeakModel<Project>,
304    ) -> impl Iterator<Item = LanguageServerId> + 'a {
305        [].into_iter()
306            .chain(self.language_servers.iter().filter_map(|(id, state)| {
307                if state.project.as_ref() == Some(project) {
308                    return Some(*id);
309                } else {
310                    None
311                }
312            }))
313            .chain(self.language_servers.iter().filter_map(|(id, state)| {
314                if state.project.is_none() {
315                    return Some(*id);
316                } else {
317                    None
318                }
319            }))
320    }
321
322    fn enable_rpc_trace_for_language_server(
323        &mut self,
324        server_id: LanguageServerId,
325    ) -> Option<&mut LanguageServerRpcState> {
326        let rpc_state = self
327            .language_servers
328            .get_mut(&server_id)?
329            .rpc_state
330            .get_or_insert_with(|| LanguageServerRpcState {
331                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
332                last_message_kind: None,
333            });
334        Some(rpc_state)
335    }
336
337    pub fn disable_rpc_trace_for_language_server(
338        &mut self,
339        server_id: LanguageServerId,
340    ) -> Option<()> {
341        self.language_servers.get_mut(&server_id)?.rpc_state.take();
342        Some(())
343    }
344
345    fn on_io(
346        &mut self,
347        language_server_id: LanguageServerId,
348        io_kind: IoKind,
349        message: &str,
350        cx: &mut ModelContext<Self>,
351    ) -> Option<()> {
352        let is_received = match io_kind {
353            IoKind::StdOut => true,
354            IoKind::StdIn => false,
355            IoKind::StdErr => {
356                let message = format!("stderr: {}", message.trim());
357                self.add_language_server_log(language_server_id, &message, cx);
358                return Some(());
359            }
360        };
361
362        let state = self
363            .get_language_server_state(language_server_id)?
364            .rpc_state
365            .as_mut()?;
366        let kind = if is_received {
367            MessageKind::Receive
368        } else {
369            MessageKind::Send
370        };
371
372        let rpc_log_lines = &mut state.rpc_messages;
373        if state.last_message_kind != Some(kind) {
374            let line_before_message = match kind {
375                MessageKind::Send => SEND_LINE,
376                MessageKind::Receive => RECEIVE_LINE,
377            };
378            rpc_log_lines.push_back(line_before_message.to_string());
379            cx.emit(Event::NewServerLogEntry {
380                id: language_server_id,
381                entry: line_before_message.to_string(),
382                is_rpc: true,
383            });
384        }
385
386        while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
387            rpc_log_lines.pop_front();
388        }
389        let message = message.trim();
390        rpc_log_lines.push_back(message.to_string());
391        cx.emit(Event::NewServerLogEntry {
392            id: language_server_id,
393            entry: message.to_string(),
394            is_rpc: true,
395        });
396        cx.notify();
397        Some(())
398    }
399}
400
401impl LspLogView {
402    pub fn new(
403        project: Model<Project>,
404        log_store: Model<LogStore>,
405        cx: &mut ViewContext<Self>,
406    ) -> Self {
407        let server_id = log_store
408            .read(cx)
409            .language_servers
410            .iter()
411            .find(|(_, server)| server.project == Some(project.downgrade()))
412            .map(|(id, _)| *id);
413
414        let weak_project = project.downgrade();
415        let model_changes_subscription = cx.observe(&log_store, move |this, store, cx| {
416            let first_server_id_for_project =
417                store.read(cx).server_ids_for_project(&weak_project).next();
418            if let Some(current_lsp) = this.current_server_id {
419                if !store.read(cx).language_servers.contains_key(&current_lsp) {
420                    if let Some(server_id) = first_server_id_for_project {
421                        if this.is_showing_rpc_trace {
422                            this.show_rpc_trace_for_server(server_id, cx)
423                        } else {
424                            this.show_logs_for_server(server_id, cx)
425                        }
426                    } else {
427                        this.current_server_id = None;
428                        this.editor.update(cx, |editor, cx| {
429                            editor.set_read_only(false);
430                            editor.clear(cx);
431                            editor.set_read_only(true);
432                        });
433                        cx.notify();
434                    }
435                }
436            } else if let Some(server_id) = first_server_id_for_project {
437                if this.is_showing_rpc_trace {
438                    this.show_rpc_trace_for_server(server_id, cx)
439                } else {
440                    this.show_logs_for_server(server_id, cx)
441                }
442            }
443
444            cx.notify();
445        });
446        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
447            Event::NewServerLogEntry { id, entry, is_rpc } => {
448                if log_view.current_server_id == Some(*id) {
449                    if (*is_rpc && log_view.is_showing_rpc_trace)
450                        || (!*is_rpc && !log_view.is_showing_rpc_trace)
451                    {
452                        log_view.editor.update(cx, |editor, cx| {
453                            editor.set_read_only(false);
454                            let last_point = editor.buffer().read(cx).len(cx);
455                            editor.edit(
456                                vec![
457                                    (last_point..last_point, entry.trim()),
458                                    (last_point..last_point, "\n"),
459                                ],
460                                cx,
461                            );
462                            editor.set_read_only(true);
463                        });
464                    }
465                }
466            }
467        });
468        let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx);
469
470        let focus_handle = cx.focus_handle();
471        let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
472            cx.focus_view(&log_view.editor);
473        });
474
475        let mut this = Self {
476            focus_handle,
477            editor,
478            editor_subscriptions,
479            project,
480            log_store,
481            current_server_id: None,
482            is_showing_rpc_trace: false,
483            _log_store_subscriptions: vec![
484                model_changes_subscription,
485                events_subscriptions,
486                focus_subscription,
487            ],
488        };
489        if let Some(server_id) = server_id {
490            this.show_logs_for_server(server_id, cx);
491        }
492        this
493    }
494
495    fn editor_for_logs(
496        log_contents: String,
497        cx: &mut ViewContext<Self>,
498    ) -> (View<Editor>, Vec<Subscription>) {
499        let editor = cx.new_view(|cx| {
500            let mut editor = Editor::multi_line(cx);
501            editor.set_text(log_contents, cx);
502            editor.move_to_end(&MoveToEnd, cx);
503            editor.set_read_only(true);
504            editor.set_show_inline_completions(false);
505            editor
506        });
507        let editor_subscription = cx.subscribe(
508            &editor,
509            |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
510                cx.emit(event.clone())
511            },
512        );
513        let search_subscription = cx.subscribe(
514            &editor,
515            |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, LspLogView>| {
516                cx.emit(event.clone())
517            },
518        );
519        (editor, vec![editor_subscription, search_subscription])
520    }
521
522    pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
523        let log_store = self.log_store.read(cx);
524
525        let mut rows = self
526            .project
527            .read(cx)
528            .language_servers()
529            .filter_map(|(server_id, language_server_name, worktree_id)| {
530                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
531                let state = log_store.language_servers.get(&server_id)?;
532                Some(LogMenuItem {
533                    server_id,
534                    server_name: language_server_name,
535                    worktree_root_name: worktree.read(cx).root_name().to_string(),
536                    rpc_trace_enabled: state.rpc_state.is_some(),
537                    rpc_trace_selected: self.is_showing_rpc_trace
538                        && self.current_server_id == Some(server_id),
539                    logs_selected: !self.is_showing_rpc_trace
540                        && self.current_server_id == Some(server_id),
541                })
542            })
543            .chain(
544                self.project
545                    .read(cx)
546                    .supplementary_language_servers()
547                    .filter_map(|(&server_id, (name, _))| {
548                        let state = log_store.language_servers.get(&server_id)?;
549                        Some(LogMenuItem {
550                            server_id,
551                            server_name: name.clone(),
552                            worktree_root_name: "supplementary".to_string(),
553                            rpc_trace_enabled: state.rpc_state.is_some(),
554                            rpc_trace_selected: self.is_showing_rpc_trace
555                                && self.current_server_id == Some(server_id),
556                            logs_selected: !self.is_showing_rpc_trace
557                                && self.current_server_id == Some(server_id),
558                        })
559                    }),
560            )
561            .chain(
562                log_store
563                    .language_servers
564                    .iter()
565                    .filter_map(|(server_id, state)| {
566                        if state.project.is_none() {
567                            Some(LogMenuItem {
568                                server_id: *server_id,
569                                server_name: state.name.clone(),
570                                worktree_root_name: "supplementary".to_string(),
571                                rpc_trace_enabled: state.rpc_state.is_some(),
572                                rpc_trace_selected: self.is_showing_rpc_trace
573                                    && self.current_server_id == Some(*server_id),
574                                logs_selected: !self.is_showing_rpc_trace
575                                    && self.current_server_id == Some(*server_id),
576                            })
577                        } else {
578                            None
579                        }
580                    }),
581            )
582            .collect::<Vec<_>>();
583        rows.sort_by_key(|row| row.server_id);
584        rows.dedup_by_key(|row| row.server_id);
585        Some(rows)
586    }
587
588    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
589        let log_contents = self
590            .log_store
591            .read(cx)
592            .server_logs(server_id)
593            .map(log_contents);
594        if let Some(log_contents) = log_contents {
595            self.current_server_id = Some(server_id);
596            self.is_showing_rpc_trace = false;
597            let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
598            self.editor = editor;
599            self.editor_subscriptions = editor_subscriptions;
600            cx.notify();
601        }
602        cx.focus(&self.focus_handle);
603    }
604
605    fn show_rpc_trace_for_server(
606        &mut self,
607        server_id: LanguageServerId,
608        cx: &mut ViewContext<Self>,
609    ) {
610        let rpc_log = self.log_store.update(cx, |log_store, _| {
611            log_store
612                .enable_rpc_trace_for_language_server(server_id)
613                .map(|state| log_contents(&state.rpc_messages))
614        });
615        if let Some(rpc_log) = rpc_log {
616            self.current_server_id = Some(server_id);
617            self.is_showing_rpc_trace = true;
618            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx);
619            let language = self.project.read(cx).languages().language_for_name("JSON");
620            editor
621                .read(cx)
622                .buffer()
623                .read(cx)
624                .as_singleton()
625                .expect("log buffer should be a singleton")
626                .update(cx, |_, cx| {
627                    cx.spawn({
628                        let buffer = cx.handle();
629                        |_, mut cx| async move {
630                            let language = language.await.ok();
631                            buffer.update(&mut cx, |buffer, cx| {
632                                buffer.set_language(language, cx);
633                            })
634                        }
635                    })
636                    .detach_and_log_err(cx);
637                });
638
639            self.editor = editor;
640            self.editor_subscriptions = editor_subscriptions;
641            cx.notify();
642        }
643
644        cx.focus(&self.focus_handle);
645    }
646
647    fn toggle_rpc_trace_for_server(
648        &mut self,
649        server_id: LanguageServerId,
650        enabled: bool,
651        cx: &mut ViewContext<Self>,
652    ) {
653        self.log_store.update(cx, |log_store, _| {
654            if enabled {
655                log_store.enable_rpc_trace_for_language_server(server_id);
656            } else {
657                log_store.disable_rpc_trace_for_language_server(server_id);
658            }
659        });
660        if !enabled && Some(server_id) == self.current_server_id {
661            self.show_logs_for_server(server_id, cx);
662            cx.notify();
663        }
664    }
665}
666
667fn log_contents(lines: &VecDeque<String>) -> String {
668    let (a, b) = lines.as_slices();
669    let log_contents = a.join("\n");
670    if b.is_empty() {
671        log_contents
672    } else {
673        log_contents + "\n" + &b.join("\n")
674    }
675}
676
677impl Render for LspLogView {
678    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
679        self.editor
680            .update(cx, |editor, cx| editor.render(cx).into_any_element())
681    }
682}
683
684impl FocusableView for LspLogView {
685    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
686        self.focus_handle.clone()
687    }
688}
689
690impl Item for LspLogView {
691    type Event = EditorEvent;
692
693    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
694        Editor::to_item_events(event, f)
695    }
696
697    fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> AnyElement {
698        Label::new("LSP Logs")
699            .color(if params.selected {
700                Color::Default
701            } else {
702                Color::Muted
703            })
704            .into_any_element()
705    }
706
707    fn telemetry_event_text(&self) -> Option<&'static str> {
708        None
709    }
710
711    fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
712        Some(Box::new(handle.clone()))
713    }
714}
715
716impl SearchableItem for LspLogView {
717    type Match = <Editor as SearchableItem>::Match;
718
719    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
720        self.editor.update(cx, |e, cx| e.clear_matches(cx))
721    }
722
723    fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
724        self.editor
725            .update(cx, |e, cx| e.update_matches(matches, cx))
726    }
727
728    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
729        self.editor.update(cx, |e, cx| e.query_suggestion(cx))
730    }
731
732    fn activate_match(
733        &mut self,
734        index: usize,
735        matches: &[Self::Match],
736        cx: &mut ViewContext<Self>,
737    ) {
738        self.editor
739            .update(cx, |e, cx| e.activate_match(index, matches, cx))
740    }
741
742    fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
743        self.editor
744            .update(cx, |e, cx| e.select_matches(matches, cx))
745    }
746
747    fn find_matches(
748        &mut self,
749        query: Arc<project::search::SearchQuery>,
750        cx: &mut ViewContext<Self>,
751    ) -> gpui::Task<Vec<Self::Match>> {
752        self.editor.update(cx, |e, cx| e.find_matches(query, cx))
753    }
754
755    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
756        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
757    }
758    fn supported_options() -> workspace::searchable::SearchOptions {
759        workspace::searchable::SearchOptions {
760            case: true,
761            word: true,
762            regex: true,
763            // LSP log is read-only.
764            replacement: false,
765        }
766    }
767    fn active_match_index(
768        &mut self,
769        matches: &[Self::Match],
770        cx: &mut ViewContext<Self>,
771    ) -> Option<usize> {
772        self.editor
773            .update(cx, |e, cx| e.active_match_index(matches, cx))
774    }
775}
776
777impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
778
779impl ToolbarItemView for LspLogToolbarItemView {
780    fn set_active_pane_item(
781        &mut self,
782        active_pane_item: Option<&dyn ItemHandle>,
783        cx: &mut ViewContext<Self>,
784    ) -> workspace::ToolbarItemLocation {
785        if let Some(item) = active_pane_item {
786            if let Some(log_view) = item.downcast::<LspLogView>() {
787                self.log_view = Some(log_view.clone());
788                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
789                    cx.notify();
790                }));
791                return ToolbarItemLocation::PrimaryLeft;
792            }
793        }
794        self.log_view = None;
795        self._log_view_subscription = None;
796        ToolbarItemLocation::Hidden
797    }
798}
799
800impl Render for LspLogToolbarItemView {
801    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
802        let Some(log_view) = self.log_view.clone() else {
803            return div();
804        };
805        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
806            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
807            let current_server_id = log_view.current_server_id;
808            (menu_rows, current_server_id)
809        });
810
811        let current_server = current_server_id.and_then(|current_server_id| {
812            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
813                Some(menu_rows[ix].clone())
814            } else {
815                None
816            }
817        });
818
819        let log_toolbar_view = cx.view().clone();
820        let lsp_menu = popover_menu("LspLogView")
821            .anchor(AnchorCorner::TopLeft)
822            .trigger(Button::new(
823                "language_server_menu_header",
824                current_server
825                    .map(|row| {
826                        Cow::Owned(format!(
827                            "{} ({}) - {}",
828                            row.server_name.0,
829                            row.worktree_root_name,
830                            if row.rpc_trace_selected {
831                                RPC_MESSAGES
832                            } else {
833                                SERVER_LOGS
834                            },
835                        ))
836                    })
837                    .unwrap_or_else(|| "No server selected".into()),
838            ))
839            .menu(move |cx| {
840                let menu_rows = menu_rows.clone();
841                let log_view = log_view.clone();
842                let log_toolbar_view = log_toolbar_view.clone();
843                ContextMenu::build(cx, move |mut menu, cx| {
844                    for (ix, row) in menu_rows.into_iter().enumerate() {
845                        let server_selected = Some(row.server_id) == current_server_id;
846                        menu = menu
847                            .header(format!(
848                                "{} ({})",
849                                row.server_name.0, row.worktree_root_name
850                            ))
851                            .entry(
852                                SERVER_LOGS,
853                                None,
854                                cx.handler_for(&log_view, move |view, cx| {
855                                    view.show_logs_for_server(row.server_id, cx);
856                                }),
857                            );
858                        if server_selected && row.logs_selected {
859                            let selected_ix = menu.select_last();
860                            debug_assert_eq!(
861                                Some(ix * 3 + 1),
862                                selected_ix,
863                                "Could not scroll to a just added LSP menu item"
864                            );
865                        }
866
867                        menu = menu.custom_entry(
868                            {
869                                let log_toolbar_view = log_toolbar_view.clone();
870                                move |cx| {
871                                    h_flex()
872                                        .w_full()
873                                        .justify_between()
874                                        .child(Label::new(RPC_MESSAGES))
875                                        .child(
876                                            div().child(
877                                                Checkbox::new(
878                                                    ix,
879                                                    if row.rpc_trace_enabled {
880                                                        Selection::Selected
881                                                    } else {
882                                                        Selection::Unselected
883                                                    },
884                                                )
885                                                .on_click(cx.listener_for(
886                                                    &log_toolbar_view,
887                                                    move |view, selection, cx| {
888                                                        let enabled = matches!(
889                                                            selection,
890                                                            Selection::Selected
891                                                        );
892                                                        view.toggle_rpc_logging_for_server(
893                                                            row.server_id,
894                                                            enabled,
895                                                            cx,
896                                                        );
897                                                        cx.stop_propagation();
898                                                    },
899                                                )),
900                                            ),
901                                        )
902                                        .into_any_element()
903                                }
904                            },
905                            cx.handler_for(&log_view, move |view, cx| {
906                                view.show_rpc_trace_for_server(row.server_id, cx);
907                            }),
908                        );
909                        if server_selected && row.rpc_trace_selected {
910                            let selected_ix = menu.select_last();
911                            debug_assert_eq!(
912                                Some(ix * 3 + 2),
913                                selected_ix,
914                                "Could not scroll to a just added LSP menu item"
915                            );
916                        }
917                    }
918                    menu
919                })
920                .into()
921            });
922
923        h_flex().size_full().child(lsp_menu).child(
924            div()
925                .child(
926                    Button::new("clear_log_button", "Clear").on_click(cx.listener(
927                        |this, _, cx| {
928                            if let Some(log_view) = this.log_view.as_ref() {
929                                log_view.update(cx, |log_view, cx| {
930                                    log_view.editor.update(cx, |editor, cx| {
931                                        editor.set_read_only(false);
932                                        editor.clear(cx);
933                                        editor.set_read_only(true);
934                                    });
935                                })
936                            }
937                        },
938                    )),
939                )
940                .ml_2(),
941        )
942    }
943}
944
945const RPC_MESSAGES: &str = "RPC Messages";
946const SERVER_LOGS: &str = "Server Logs";
947
948impl LspLogToolbarItemView {
949    pub fn new() -> Self {
950        Self {
951            log_view: None,
952            _log_view_subscription: None,
953        }
954    }
955
956    fn toggle_rpc_logging_for_server(
957        &mut self,
958        id: LanguageServerId,
959        enabled: bool,
960        cx: &mut ViewContext<Self>,
961    ) {
962        if let Some(log_view) = &self.log_view {
963            log_view.update(cx, |log_view, cx| {
964                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
965                if !enabled && Some(id) == log_view.current_server_id {
966                    log_view.show_logs_for_server(id, cx);
967                    cx.notify();
968                } else if enabled {
969                    log_view.show_rpc_trace_for_server(id, cx);
970                    cx.notify();
971                }
972                cx.focus(&log_view.focus_handle);
973            });
974        }
975        cx.notify();
976    }
977}
978
979pub enum Event {
980    NewServerLogEntry {
981        id: LanguageServerId,
982        entry: String,
983        is_rpc: bool,
984    },
985}
986
987impl EventEmitter<Event> for LogStore {}
988impl EventEmitter<Event> for LspLogView {}
989impl EventEmitter<EditorEvent> for LspLogView {}
990impl EventEmitter<SearchEvent> for LspLogView {}