Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:52:02.939Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

lsp_log_view.rs

1384 lines · 56.0 KB · rust
1use collections::VecDeque;
2use edit_prediction::EditPredictionStore;
3use editor::{Editor, EditorEvent, MultiBufferOffset, actions::MoveToEnd, scroll::Autoscroll};
4use gpui::{
5    Anchor, App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement,
6    Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
7};
8use itertools::Itertools as _;
9use language::{LanguageServerId, language_settings::SoftWrap};
10use lsp::{
11    LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, SetTraceParams,
12    TraceValue, notification::SetTrace,
13};
14use project::{
15    LanguageServerStatus, Project,
16    lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message},
17    search::SearchQuery,
18};
19use proto::toggle_lsp_logs::LogType;
20use settings::SeedQuerySetting;
21use std::{any::TypeId, borrow::Cow, sync::Arc};
22use ui::{Checkbox, ContextMenu, PopoverMenu, ToggleState, prelude::*};
23use util::ResultExt as _;
24use workspace::{
25    SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
26    item::{Item, ItemHandle},
27    searchable::{Direction, SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},
28};
29
30use crate::get_or_create_tool;
31
32pub fn open(
33    log_store: &Entity<LogStore>,
34    workspace: WeakEntity<Workspace>,
35    server: LanguageServerSelector,
36    window: &mut Window,
37    cx: &mut App,
38) {
39    log_store.update(cx, |_, cx| {
40        cx.spawn_in(window, async move |log_store, cx| {
41            let Some(log_store) = log_store.upgrade() else {
42                return;
43            };
44            workspace
45                .update_in(cx, |workspace, window, cx| {
46                    let project = workspace.project().clone();
47                    let tool_log_store = log_store.clone();
48                    let log_view = get_or_create_tool(
49                        workspace,
50                        SplitDirection::Right,
51                        window,
52                        cx,
53                        move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
54                    );
55                    log_view.update(cx, |log_view, cx| {
56                        let server_id = match server {
57                            LanguageServerSelector::Id(id) => Some(id),
58                            LanguageServerSelector::Name(name) => {
59                                log_store.read(cx).language_servers.iter().find_map(
60                                    |(id, state)| {
61                                        if state.name.as_ref() == Some(&name) {
62                                            Some(*id)
63                                        } else {
64                                            None
65                                        }
66                                    },
67                                )
68                            }
69                        };
70                        if let Some(server_id) = server_id {
71                            log_view.show_logs_for_server(server_id, window, cx);
72                        }
73                    });
74                })
75                .ok();
76        })
77        .detach();
78    })
79}
80
81pub struct LspLogView {
82    pub(crate) editor: Entity<Editor>,
83    editor_subscriptions: Vec<Subscription>,
84    log_store: Entity<LogStore>,
85    current_server_id: Option<LanguageServerId>,
86    active_entry_kind: LogKind,
87    project: Entity<Project>,
88    focus_handle: FocusHandle,
89    _log_store_subscriptions: Vec<Subscription>,
90}
91
92pub struct LspLogToolbarItemView {
93    log_view: Option<Entity<LspLogView>>,
94    _log_view_subscription: Option<Subscription>,
95}
96
97#[derive(Clone, Debug, PartialEq)]
98pub(crate) struct LogMenuItem {
99    pub server_id: LanguageServerId,
100    pub server_name: LanguageServerName,
101    pub worktree_root_name: String,
102    pub rpc_trace_enabled: bool,
103    pub selected_entry: LogKind,
104    pub trace_level: lsp::TraceValue,
105    pub server_kind: LanguageServerKind,
106}
107
108actions!(
109    dev,
110    [
111        /// Opens the language server protocol logs viewer.
112        OpenLanguageServerLogs
113    ]
114);
115
116pub fn init(on_headless_host: bool, cx: &mut App) {
117    let log_store = log_store::init(on_headless_host, cx);
118
119    cx.observe_new(move |workspace: &mut Workspace, _, cx| {
120        log_store.update(cx, |store, cx| {
121            store.add_project(workspace.project(), cx);
122        });
123
124        let log_store = log_store.clone();
125        workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
126            let log_store = log_store.clone();
127            let project = workspace.project().clone();
128            get_or_create_tool(
129                workspace,
130                SplitDirection::Right,
131                window,
132                cx,
133                move |window, cx| LspLogView::new(project, log_store, window, cx),
134            );
135        });
136    })
137    .detach();
138}
139
140impl LspLogView {
141    pub fn new(
142        project: Entity<Project>,
143        log_store: Entity<LogStore>,
144        window: &mut Window,
145        cx: &mut Context<Self>,
146    ) -> Self {
147        let server_id = log_store
148            .read(cx)
149            .language_servers
150            .iter()
151            .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
152            .map(|(id, _)| *id);
153
154        let weak_project = project.downgrade();
155        let model_changes_subscription =
156            cx.observe_in(&log_store, window, move |this, store, window, cx| {
157                let first_server_id_for_project =
158                    store.read(cx).server_ids_for_project(&weak_project).next();
159                if let Some(current_lsp) = this.current_server_id {
160                    if !store.read(cx).language_servers.contains_key(&current_lsp)
161                        && let Some(server_id) = first_server_id_for_project
162                    {
163                        match this.active_entry_kind {
164                            LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
165                            LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
166                            LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
167                            LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
168                        }
169                    }
170                } else if let Some(server_id) = first_server_id_for_project {
171                    match this.active_entry_kind {
172                        LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
173                        LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
174                        LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
175                        LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
176                    }
177                }
178
179                cx.notify();
180            });
181
182        let events_subscriptions = cx.subscribe_in(
183            &log_store,
184            window,
185            move |log_view, _, e, window, cx| match e {
186                Event::NewServerLogEntry { id, kind, text } => {
187                    if log_view.current_server_id == Some(*id)
188                        && LogKind::from_server_log_type(kind) == log_view.active_entry_kind
189                    {
190                        log_view.editor.update(cx, |editor, cx| {
191                            editor.set_read_only(false);
192                            let last_offset = editor.buffer().read(cx).len(cx);
193                            let newest_cursor_is_at_end = editor
194                                .selections
195                                .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
196                                .start
197                                >= last_offset;
198                            editor.edit(
199                                vec![
200                                    (last_offset..last_offset, text.as_str()),
201                                    (last_offset..last_offset, "\n"),
202                                ],
203                                cx,
204                            );
205                            if text.len() > 1024 {
206                                let b = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
207                                let fold_offset =
208                                    b.as_rope().ceil_char_boundary(last_offset.0 + 1024);
209                                editor.fold_ranges(
210                                    vec![
211                                        MultiBufferOffset(fold_offset)
212                                            ..MultiBufferOffset(b.as_rope().len()),
213                                    ],
214                                    false,
215                                    window,
216                                    cx,
217                                );
218                            }
219
220                            if newest_cursor_is_at_end {
221                                editor.request_autoscroll(Autoscroll::bottom(), cx);
222                            }
223                            editor.set_read_only(true);
224                        });
225                    }
226                }
227            },
228        );
229        let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
230
231        let focus_handle = cx.focus_handle();
232        let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
233            window.focus(&log_view.editor.focus_handle(cx), cx);
234        });
235
236        cx.on_release(|log_view, cx| {
237            log_view.log_store.update(cx, |log_store, cx| {
238                for (server_id, state) in &log_store.language_servers {
239                    if let Some(log_kind) = state.toggled_log_kind {
240                        if let Some(log_type) = log_type(log_kind) {
241                            send_toggle_log_message(state, *server_id, false, log_type, cx);
242                        }
243                    }
244                }
245            });
246        })
247        .detach();
248
249        let mut lsp_log_view = Self {
250            focus_handle,
251            editor,
252            editor_subscriptions,
253            project,
254            log_store,
255            current_server_id: None,
256            active_entry_kind: LogKind::Logs,
257            _log_store_subscriptions: vec![
258                model_changes_subscription,
259                events_subscriptions,
260                focus_subscription,
261            ],
262        };
263        if let Some(server_id) = server_id {
264            lsp_log_view.show_logs_for_server(server_id, window, cx);
265        }
266        lsp_log_view
267    }
268
269    fn editor_for_logs(
270        log_contents: String,
271        window: &mut Window,
272        cx: &mut Context<Self>,
273    ) -> (Entity<Editor>, Vec<Subscription>) {
274        let editor = initialize_new_editor(log_contents, true, window, cx);
275        let editor_subscription = cx.subscribe(
276            &editor,
277            |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
278        );
279        let search_subscription = cx.subscribe(
280            &editor,
281            |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
282        );
283        (editor, vec![editor_subscription, search_subscription])
284    }
285
286    fn editor_for_server_info(
287        info: ServerInfo,
288        window: &mut Window,
289        cx: &mut Context<Self>,
290    ) -> (Entity<Editor>, Vec<Subscription>) {
291        let server_info = format!(
292            "* Server: {NAME} (id {ID})
293
294* Version: {VERSION}
295
296* Binary: {BINARY}
297
298* Registered workspace folders:
299{WORKSPACE_FOLDERS}
300
301* Capabilities: {CAPABILITIES}
302
303* Configuration: {CONFIGURATION}",
304            NAME = info.status.name,
305            ID = info.id,
306            VERSION = info
307                .status
308                .server_version
309                .as_ref()
310                .map(|version| version.as_ref())
311                .unwrap_or("Unknown"),
312            BINARY = info
313                .status
314                .binary
315                .as_ref()
316                .map_or_else(|| "Unknown".to_string(), |binary| format!("{:#?}", binary)),
317            WORKSPACE_FOLDERS = info
318                .status
319                .workspace_folders
320                .iter()
321                .filter_map(|uri| uri.to_file_path().ok())
322                .map(|path| path.to_string_lossy().into_owned())
323                .join(", "),
324            CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
325                .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
326            CONFIGURATION = info
327                .status
328                .configuration
329                .map(|configuration| serde_json::to_string_pretty(&configuration))
330                .transpose()
331                .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}")))
332                .unwrap_or_else(|| "Unknown".to_string()),
333        );
334        let editor = initialize_new_editor(server_info, false, window, cx);
335        let editor_subscription = cx.subscribe(
336            &editor,
337            |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
338        );
339        let search_subscription = cx.subscribe(
340            &editor,
341            |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
342        );
343        (editor, vec![editor_subscription, search_subscription])
344    }
345    pub(crate) fn try_ensure_copilot_for_project(&self, cx: &mut App) {
346        self.log_store.update(cx, |this, cx| {
347            let copilot = EditPredictionStore::try_global(cx)
348                .and_then(|store| store.read(cx).copilot_for_project(&self.project))?;
349            let server = copilot.read(cx).language_server()?.clone();
350            let log_subscription = this.copilot_state_for_project(&self.project.downgrade());
351            if let Some(subscription_slot @ None) = log_subscription {
352                let weak_lsp_store = cx.weak_entity();
353                let server_id = server.server_id();
354
355                let name = LanguageServerName::new_static("copilot");
356                *subscription_slot =
357                    Some(server.on_notification::<lsp::notification::LogMessage, _>(
358                        move |params, cx| {
359                            weak_lsp_store
360                                .update(cx, |lsp_store, cx| {
361                                    lsp_store.add_language_server_log(
362                                        server_id,
363                                        MessageType::LOG,
364                                        &params.message,
365                                        cx,
366                                    );
367                                })
368                                .ok();
369                        },
370                    ));
371                this.add_language_server(
372                    LanguageServerKind::Global,
373                    server.server_id(),
374                    Some(name),
375                    None,
376                    Some(server.clone()),
377                    cx,
378                );
379            }
380
381            Some(())
382        });
383    }
384    pub(crate) fn menu_items(&self, cx: &mut App) -> Option<Vec<LogMenuItem>> {
385        self.try_ensure_copilot_for_project(cx);
386        let log_store = self.log_store.read(cx);
387
388        let unknown_server = LanguageServerName::new_static("unknown server");
389
390        let mut rows = log_store
391            .language_servers
392            .iter()
393            .map(|(server_id, state)| match &state.kind {
394                LanguageServerKind::Local { .. }
395                | LanguageServerKind::Remote { .. }
396                | LanguageServerKind::LocalSsh { .. } => {
397                    let worktree_root_name = state
398                        .worktree_id
399                        .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
400                        .map(|worktree| worktree.read(cx).root_name_str().to_string())
401                        .unwrap_or_else(|| "Unknown worktree".to_string());
402
403                    LogMenuItem {
404                        server_id: *server_id,
405                        server_name: state.name.clone().unwrap_or(unknown_server.clone()),
406                        server_kind: state.kind.clone(),
407                        worktree_root_name,
408                        rpc_trace_enabled: state.rpc_state.is_some(),
409                        selected_entry: self.active_entry_kind,
410                        trace_level: lsp::TraceValue::Off,
411                    }
412                }
413
414                LanguageServerKind::Global => LogMenuItem {
415                    server_id: *server_id,
416                    server_name: state.name.clone().unwrap_or(unknown_server.clone()),
417                    server_kind: state.kind.clone(),
418                    worktree_root_name: "supplementary".to_string(),
419                    rpc_trace_enabled: state.rpc_state.is_some(),
420                    selected_entry: self.active_entry_kind,
421                    trace_level: lsp::TraceValue::Off,
422                },
423            })
424            .chain(
425                self.project
426                    .read(cx)
427                    .supplementary_language_servers(cx)
428                    .filter_map(|(server_id, name)| {
429                        let state = log_store.language_servers.get(&server_id)?;
430                        Some(LogMenuItem {
431                            server_id,
432                            server_name: name,
433                            server_kind: state.kind.clone(),
434                            worktree_root_name: "supplementary".to_string(),
435                            rpc_trace_enabled: state.rpc_state.is_some(),
436                            selected_entry: self.active_entry_kind,
437                            trace_level: lsp::TraceValue::Off,
438                        })
439                    }),
440            )
441            .collect::<Vec<_>>();
442        rows.sort_by_key(|row| row.server_id);
443        rows.dedup_by_key(|row| row.server_id);
444        Some(rows)
445    }
446
447    fn show_logs_for_server(
448        &mut self,
449        server_id: LanguageServerId,
450        window: &mut Window,
451        cx: &mut Context<Self>,
452    ) {
453        let typ = self
454            .log_store
455            .read(cx)
456            .language_servers
457            .get(&server_id)
458            .map(|v| v.log_level)
459            .unwrap_or(MessageType::LOG);
460        let log_contents = self
461            .log_store
462            .read(cx)
463            .server_logs(server_id)
464            .map(|v| log_contents(v, typ));
465        if let Some(log_contents) = log_contents {
466            self.current_server_id = Some(server_id);
467            self.active_entry_kind = LogKind::Logs;
468            let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
469            self.editor = editor;
470            self.editor_subscriptions = editor_subscriptions;
471            cx.notify();
472        }
473        self.editor.read(cx).focus_handle(cx).focus(window, cx);
474        self.log_store.update(cx, |log_store, cx| {
475            let state = log_store.get_language_server_state(server_id)?;
476            state.toggled_log_kind = Some(LogKind::Logs);
477            send_toggle_log_message(state, server_id, true, LogType::Log, cx);
478            Some(())
479        });
480    }
481
482    fn update_log_level(
483        &self,
484        server_id: LanguageServerId,
485        level: MessageType,
486        window: &mut Window,
487        cx: &mut Context<Self>,
488    ) {
489        let log_contents = self.log_store.update(cx, |this, _| {
490            if let Some(state) = this.get_language_server_state(server_id) {
491                state.log_level = level;
492            }
493
494            this.server_logs(server_id).map(|v| log_contents(v, level))
495        });
496
497        if let Some(log_contents) = log_contents {
498            self.editor.update(cx, |editor, cx| {
499                editor.set_text(log_contents, window, cx);
500                editor.move_to_end(&MoveToEnd, window, cx);
501            });
502            cx.notify();
503        }
504
505        self.editor.read(cx).focus_handle(cx).focus(window, cx);
506    }
507
508    fn show_trace_for_server(
509        &mut self,
510        server_id: LanguageServerId,
511        window: &mut Window,
512        cx: &mut Context<Self>,
513    ) {
514        let trace_level = self
515            .log_store
516            .update(cx, |log_store, _| {
517                Some(log_store.get_language_server_state(server_id)?.trace_level)
518            })
519            .unwrap_or(TraceValue::Messages);
520        let log_contents = self
521            .log_store
522            .read(cx)
523            .server_trace(server_id)
524            .map(|v| log_contents(v, trace_level));
525        if let Some(log_contents) = log_contents {
526            self.current_server_id = Some(server_id);
527            self.active_entry_kind = LogKind::Trace;
528            let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
529            self.editor = editor;
530            self.editor_subscriptions = editor_subscriptions;
531            self.log_store.update(cx, |log_store, cx| {
532                let state = log_store.get_language_server_state(server_id)?;
533                state.toggled_log_kind = Some(LogKind::Trace);
534                send_toggle_log_message(state, server_id, true, LogType::Trace, cx);
535                Some(())
536            });
537            cx.notify();
538        }
539        self.editor.read(cx).focus_handle(cx).focus(window, cx);
540    }
541
542    fn show_rpc_trace_for_server(
543        &mut self,
544        server_id: LanguageServerId,
545        window: &mut Window,
546        cx: &mut Context<Self>,
547    ) {
548        self.toggle_rpc_trace_for_server(server_id, true, window, cx);
549        let rpc_log = self.log_store.update(cx, |log_store, _| {
550            log_store
551                .enable_rpc_trace_for_language_server(server_id)
552                .map(|state| log_contents(&state.rpc_messages, ()))
553        });
554        if let Some(rpc_log) = rpc_log {
555            self.current_server_id = Some(server_id);
556            self.active_entry_kind = LogKind::Rpc;
557            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
558            let language = self.project.read(cx).languages().language_for_name("JSON");
559            editor
560                .read(cx)
561                .buffer()
562                .read(cx)
563                .as_singleton()
564                .expect("log buffer should be a singleton")
565                .update(cx, |_, cx| {
566                    cx.spawn({
567                        let buffer = cx.entity();
568                        async move |_, cx| {
569                            let language = language.await.ok();
570                            buffer.update(cx, |buffer, cx| {
571                                buffer.set_language(language, cx);
572                            });
573                        }
574                    })
575                    .detach();
576                });
577
578            self.editor = editor;
579            self.editor_subscriptions = editor_subscriptions;
580            cx.notify();
581        }
582
583        self.editor.read(cx).focus_handle(cx).focus(window, cx);
584    }
585
586    fn toggle_rpc_trace_for_server(
587        &mut self,
588        server_id: LanguageServerId,
589        enabled: bool,
590        window: &mut Window,
591        cx: &mut Context<Self>,
592    ) {
593        self.log_store.update(cx, |log_store, cx| {
594            if enabled {
595                log_store.enable_rpc_trace_for_language_server(server_id);
596            } else {
597                log_store.disable_rpc_trace_for_language_server(server_id);
598            }
599
600            if let Some(server_state) = log_store.language_servers.get(&server_id) {
601                send_toggle_log_message(server_state, server_id, enabled, LogType::Rpc, cx);
602            };
603        });
604        if !enabled && Some(server_id) == self.current_server_id {
605            self.show_logs_for_server(server_id, window, cx);
606            cx.notify();
607        }
608    }
609
610    fn update_trace_level(
611        &self,
612        server_id: LanguageServerId,
613        level: TraceValue,
614        cx: &mut Context<Self>,
615    ) {
616        if let Some(server) = self
617            .project
618            .read(cx)
619            .lsp_store()
620            .read(cx)
621            .language_server_for_id(server_id)
622        {
623            self.log_store.update(cx, |this, _| {
624                if let Some(state) = this.get_language_server_state(server_id) {
625                    state.trace_level = level;
626                }
627            });
628
629            server
630                .notify::<SetTrace>(SetTraceParams { value: level })
631                .ok();
632        }
633    }
634
635    fn show_server_info(
636        &mut self,
637        server_id: LanguageServerId,
638        window: &mut Window,
639        cx: &mut Context<Self>,
640    ) {
641        let Some(server_info) = self
642            .project
643            .read(cx)
644            .lsp_store()
645            .update(cx, |lsp_store, _| {
646                lsp_store
647                    .language_server_for_id(server_id)
648                    .as_ref()
649                    .map(|language_server| ServerInfo::new(language_server))
650                    .or_else(move || {
651                        let capabilities =
652                            lsp_store.lsp_server_capabilities.get(&server_id)?.clone();
653                        let status = lsp_store.language_server_statuses.get(&server_id)?.clone();
654
655                        Some(ServerInfo {
656                            id: server_id,
657                            capabilities,
658                            status,
659                        })
660                    })
661            })
662        else {
663            return;
664        };
665        self.current_server_id = Some(server_id);
666        self.active_entry_kind = LogKind::ServerInfo;
667        let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx);
668        self.editor = editor;
669        self.editor_subscriptions = editor_subscriptions;
670        cx.notify();
671        self.editor.read(cx).focus_handle(cx).focus(window, cx);
672        self.log_store.update(cx, |log_store, cx| {
673            let state = log_store.get_language_server_state(server_id)?;
674            if let Some(log_kind) = state.toggled_log_kind.take() {
675                if let Some(log_type) = log_type(log_kind) {
676                    send_toggle_log_message(state, server_id, false, log_type, cx);
677                }
678            };
679            Some(())
680        });
681    }
682}
683
684fn log_type(log_kind: LogKind) -> Option<LogType> {
685    match log_kind {
686        LogKind::Rpc => Some(LogType::Rpc),
687        LogKind::Trace => Some(LogType::Trace),
688        LogKind::Logs => Some(LogType::Log),
689        LogKind::ServerInfo => None,
690    }
691}
692
693fn send_toggle_log_message(
694    server_state: &log_store::LanguageServerState,
695    server_id: LanguageServerId,
696    enabled: bool,
697    log_type: LogType,
698    cx: &mut App,
699) {
700    if let LanguageServerKind::Remote { project } = &server_state.kind {
701        project
702            .update(cx, |project, cx| {
703                if let Some((client, project_id)) = project.lsp_store().read(cx).upstream_client() {
704                    client
705                        .send(proto::ToggleLspLogs {
706                            project_id,
707                            log_type: log_type as i32,
708                            server_id: server_id.to_proto(),
709                            enabled,
710                        })
711                        .log_err();
712                }
713            })
714            .ok();
715    }
716}
717
718fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
719    lines
720        .iter()
721        .filter(|message| message.should_include(level))
722        .flat_map(|message| [message.as_ref(), "\n"])
723        .collect()
724}
725
726impl Render for LspLogView {
727    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
728        self.editor.update(cx, |editor, cx| {
729            editor.render(window, cx).into_any_element()
730        })
731    }
732}
733
734impl Focusable for LspLogView {
735    fn focus_handle(&self, _: &App) -> FocusHandle {
736        self.focus_handle.clone()
737    }
738}
739
740impl Item for LspLogView {
741    type Event = EditorEvent;
742
743    fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
744        Editor::to_item_events(event, f)
745    }
746
747    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
748        "LSP Logs".into()
749    }
750
751    fn telemetry_event_text(&self) -> Option<&'static str> {
752        None
753    }
754
755    fn as_searchable(
756        &self,
757        handle: &Entity<Self>,
758        _: &App,
759    ) -> Option<Box<dyn SearchableItemHandle>> {
760        Some(Box::new(handle.clone()))
761    }
762
763    fn act_as_type<'a>(
764        &'a self,
765        type_id: TypeId,
766        self_handle: &'a Entity<Self>,
767        _: &'a App,
768    ) -> Option<gpui::AnyEntity> {
769        if type_id == TypeId::of::<Self>() {
770            Some(self_handle.clone().into())
771        } else if type_id == TypeId::of::<Editor>() {
772            Some(self.editor.clone().into())
773        } else {
774            None
775        }
776    }
777
778    fn can_split(&self) -> bool {
779        true
780    }
781
782    fn clone_on_split(
783        &self,
784        _workspace_id: Option<WorkspaceId>,
785        window: &mut Window,
786        cx: &mut Context<Self>,
787    ) -> Task<Option<Entity<Self>>>
788    where
789        Self: Sized,
790    {
791        Task::ready(Some(cx.new(|cx| {
792            let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
793            if let Some(server_id) = self.current_server_id {
794                match self.active_entry_kind {
795                    LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
796                    LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
797                    LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
798                    LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
799                }
800            }
801            new_view
802        })))
803    }
804}
805
806impl SearchableItem for LspLogView {
807    type Match = <Editor as SearchableItem>::Match;
808
809    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
810        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
811    }
812
813    fn update_matches(
814        &mut self,
815        matches: &[Self::Match],
816        active_match_index: Option<usize>,
817        token: SearchToken,
818        window: &mut Window,
819        cx: &mut Context<Self>,
820    ) {
821        self.editor.update(cx, |e, cx| {
822            e.update_matches(matches, active_match_index, token, window, cx)
823        })
824    }
825
826    fn query_suggestion(
827        &mut self,
828        seed_query_override: Option<SeedQuerySetting>,
829        window: &mut Window,
830        cx: &mut Context<Self>,
831    ) -> String {
832        self.editor.update(cx, |e, cx| {
833            e.query_suggestion(seed_query_override, window, cx)
834        })
835    }
836
837    fn activate_match(
838        &mut self,
839        index: usize,
840        matches: &[Self::Match],
841        token: SearchToken,
842        window: &mut Window,
843        cx: &mut Context<Self>,
844    ) {
845        self.editor.update(cx, |e, cx| {
846            e.activate_match(index, matches, token, window, cx)
847        })
848    }
849
850    fn select_matches(
851        &mut self,
852        matches: &[Self::Match],
853        token: SearchToken,
854        window: &mut Window,
855        cx: &mut Context<Self>,
856    ) {
857        self.editor
858            .update(cx, |e, cx| e.select_matches(matches, token, window, cx))
859    }
860
861    fn find_matches(
862        &mut self,
863        query: Arc<project::search::SearchQuery>,
864        window: &mut Window,
865        cx: &mut Context<Self>,
866    ) -> gpui::Task<Vec<Self::Match>> {
867        self.editor
868            .update(cx, |e, cx| e.find_matches(query, window, cx))
869    }
870
871    fn replace(
872        &mut self,
873        _: &Self::Match,
874        _: &SearchQuery,
875        _token: SearchToken,
876        _window: &mut Window,
877        _: &mut Context<Self>,
878    ) {
879        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
880    }
881    fn supported_options(&self) -> workspace::searchable::SearchOptions {
882        workspace::searchable::SearchOptions {
883            case: true,
884            word: true,
885            regex: true,
886            find_in_results: false,
887            // LSP log is read-only.
888            replacement: false,
889            selection: false,
890            select_all: true,
891        }
892    }
893    fn active_match_index(
894        &mut self,
895        direction: Direction,
896        matches: &[Self::Match],
897        token: SearchToken,
898        window: &mut Window,
899        cx: &mut Context<Self>,
900    ) -> Option<usize> {
901        self.editor.update(cx, |e, cx| {
902            e.active_match_index(direction, matches, token, window, cx)
903        })
904    }
905}
906
907impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
908
909impl ToolbarItemView for LspLogToolbarItemView {
910    fn set_active_pane_item(
911        &mut self,
912        active_pane_item: Option<&dyn ItemHandle>,
913        _: &mut Window,
914        cx: &mut Context<Self>,
915    ) -> workspace::ToolbarItemLocation {
916        if let Some(item) = active_pane_item
917            && let Some(log_view) = item.downcast::<LspLogView>()
918        {
919            self.log_view = Some(log_view.clone());
920            self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
921                cx.notify();
922            }));
923            return ToolbarItemLocation::PrimaryLeft;
924        }
925        self.log_view = None;
926        self._log_view_subscription = None;
927        ToolbarItemLocation::Hidden
928    }
929}
930
931impl Render for LspLogToolbarItemView {
932    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
933        let Some(log_view) = self.log_view.clone() else {
934            return div();
935        };
936
937        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
938            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
939            let current_server_id = log_view.current_server_id;
940            (menu_rows, current_server_id)
941        });
942
943        let current_server = current_server_id.and_then(|current_server_id| {
944            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
945                Some(menu_rows[ix].clone())
946            } else {
947                None
948            }
949        });
950
951        let available_language_servers: Vec<_> = menu_rows
952            .into_iter()
953            .map(|row| {
954                (
955                    row.server_id,
956                    row.server_name,
957                    row.worktree_root_name,
958                    row.selected_entry,
959                )
960            })
961            .collect();
962
963        let log_toolbar_view = cx.weak_entity();
964
965        let lsp_menu = PopoverMenu::new("LspLogView")
966            .anchor(Anchor::TopLeft)
967            .trigger(
968                Button::new(
969                    "language_server_menu_header",
970                    current_server
971                        .as_ref()
972                        .map(|row| {
973                            Cow::Owned(format!(
974                                "{} ({})",
975                                row.server_name.0, row.worktree_root_name,
976                            ))
977                        })
978                        .unwrap_or_else(|| "No server selected".into()),
979                )
980                .end_icon(
981                    Icon::new(IconName::ChevronDown)
982                        .size(IconSize::Small)
983                        .color(Color::Muted),
984                ),
985            )
986            .menu({
987                let log_view = log_view.clone();
988                move |window, cx| {
989                    let log_view = log_view.clone();
990                    ContextMenu::build(window, cx, |mut menu, window, _| {
991                        for (server_id, name, worktree_root, active_entry_kind) in
992                            available_language_servers.iter()
993                        {
994                            let label = format!("{name} ({worktree_root})");
995                            let server_id = *server_id;
996                            let active_entry_kind = *active_entry_kind;
997                            menu = menu.entry(
998                                label,
999                                None,
1000                                window.handler_for(&log_view, move |view, window, cx| {
1001                                    view.current_server_id = Some(server_id);
1002                                    view.active_entry_kind = active_entry_kind;
1003                                    match view.active_entry_kind {
1004                                        LogKind::Rpc => {
1005                                            view.toggle_rpc_trace_for_server(
1006                                                server_id, true, window, cx,
1007                                            );
1008                                            view.show_rpc_trace_for_server(server_id, window, cx);
1009                                        }
1010                                        LogKind::Trace => {
1011                                            view.show_trace_for_server(server_id, window, cx)
1012                                        }
1013                                        LogKind::Logs => {
1014                                            view.show_logs_for_server(server_id, window, cx)
1015                                        }
1016                                        LogKind::ServerInfo => {
1017                                            view.show_server_info(server_id, window, cx)
1018                                        }
1019                                    }
1020                                    cx.notify();
1021                                }),
1022                            );
1023                        }
1024                        menu
1025                    })
1026                    .into()
1027                }
1028            });
1029
1030        let view_selector = current_server.map(|server| {
1031            let server_id = server.server_id;
1032            let rpc_trace_enabled = server.rpc_trace_enabled;
1033            let log_view = log_view.clone();
1034            let label = match server.selected_entry {
1035                LogKind::Rpc => RPC_MESSAGES,
1036                LogKind::Trace => SERVER_TRACE,
1037                LogKind::Logs => SERVER_LOGS,
1038                LogKind::ServerInfo => SERVER_INFO,
1039            };
1040            PopoverMenu::new("LspViewSelector")
1041                .anchor(Anchor::TopLeft)
1042                .trigger(
1043                    Button::new("language_server_menu_header", label).end_icon(
1044                        Icon::new(IconName::ChevronDown)
1045                            .size(IconSize::Small)
1046                            .color(Color::Muted),
1047                    ),
1048                )
1049                .menu(move |window, cx| {
1050                    let log_toolbar_view = log_toolbar_view.upgrade()?;
1051                    let log_view = log_view.clone();
1052                    Some(ContextMenu::build(window, cx, move |this, window, _| {
1053                        this.entry(
1054                            SERVER_LOGS,
1055                            None,
1056                            window.handler_for(&log_view, move |view, window, cx| {
1057                                view.show_logs_for_server(server_id, window, cx);
1058                            }),
1059                        )
1060                        .entry(
1061                            SERVER_TRACE,
1062                            None,
1063                            window.handler_for(&log_view, move |view, window, cx| {
1064                                view.show_trace_for_server(server_id, window, cx);
1065                            }),
1066                        )
1067                        .custom_entry(
1068                            {
1069                                let log_toolbar_view = log_toolbar_view.clone();
1070                                move |window, _| {
1071                                    h_flex()
1072                                        .w_full()
1073                                        .justify_between()
1074                                        .child(Label::new(RPC_MESSAGES))
1075                                        .child(
1076                                            div().child(
1077                                                Checkbox::new(
1078                                                    "LspLogEnableRpcTrace",
1079                                                    if rpc_trace_enabled {
1080                                                        ToggleState::Selected
1081                                                    } else {
1082                                                        ToggleState::Unselected
1083                                                    },
1084                                                )
1085                                                .on_click(window.listener_for(
1086                                                    &log_toolbar_view,
1087                                                    move |view, selection, window, cx| {
1088                                                        let enabled = matches!(
1089                                                            selection,
1090                                                            ToggleState::Selected
1091                                                        );
1092                                                        view.toggle_rpc_logging_for_server(
1093                                                            server_id, enabled, window, cx,
1094                                                        );
1095                                                        cx.stop_propagation();
1096                                                    },
1097                                                )),
1098                                            ),
1099                                        )
1100                                        .into_any_element()
1101                                }
1102                            },
1103                            window.handler_for(&log_view, move |view, window, cx| {
1104                                view.show_rpc_trace_for_server(server_id, window, cx);
1105                            }),
1106                        )
1107                        .entry(
1108                            SERVER_INFO,
1109                            None,
1110                            window.handler_for(&log_view, move |view, window, cx| {
1111                                view.show_server_info(server_id, window, cx);
1112                            }),
1113                        )
1114                    }))
1115                })
1116        });
1117
1118        h_flex()
1119            .size_full()
1120            .gap_1()
1121            .justify_between()
1122            .child(
1123                h_flex()
1124                    .gap_0p5()
1125                    .child(lsp_menu)
1126                    .children(view_selector)
1127                    .child(
1128                        log_view.update(cx, |this, _cx| match this.active_entry_kind {
1129                            LogKind::Trace => {
1130                                let log_view = log_view.clone();
1131                                div().child(
1132                                    PopoverMenu::new("lsp-trace-level-menu")
1133                                        .anchor(Anchor::TopLeft)
1134                                        .trigger(
1135                                            Button::new(
1136                                                "language_server_trace_level_selector",
1137                                                "Trace level",
1138                                            )
1139                                            .end_icon(
1140                                                Icon::new(IconName::ChevronDown)
1141                                                    .size(IconSize::Small)
1142                                                    .color(Color::Muted),
1143                                            ),
1144                                        )
1145                                        .menu({
1146                                            let log_view = log_view;
1147
1148                                            move |window, cx| {
1149                                                let id = log_view.read(cx).current_server_id?;
1150
1151                                                let trace_level =
1152                                                    log_view.update(cx, |this, cx| {
1153                                                        this.log_store.update(cx, |this, _| {
1154                                                            Some(
1155                                                                this.get_language_server_state(id)?
1156                                                                    .trace_level,
1157                                                            )
1158                                                        })
1159                                                    })?;
1160
1161                                                ContextMenu::build(
1162                                                    window,
1163                                                    cx,
1164                                                    |mut menu, window, cx| {
1165                                                        let log_view = log_view.clone();
1166
1167                                                        for (option, label) in [
1168                                                            (TraceValue::Off, "Off"),
1169                                                            (TraceValue::Messages, "Messages"),
1170                                                            (TraceValue::Verbose, "Verbose"),
1171                                                        ] {
1172                                                            menu = menu.entry(label, None, {
1173                                                                let log_view = log_view.clone();
1174                                                                move |_, cx| {
1175                                                                    log_view.update(cx, |this, cx| {
1176                                                                    if let Some(id) =
1177                                                                        this.current_server_id
1178                                                                    {
1179                                                                        this.update_trace_level(
1180                                                                            id, option, cx,
1181                                                                        );
1182                                                                    }
1183                                                                });
1184                                                                }
1185                                                            });
1186                                                            if option == trace_level {
1187                                                                menu.select_last(window, cx);
1188                                                            }
1189                                                        }
1190
1191                                                        menu
1192                                                    },
1193                                                )
1194                                                .into()
1195                                            }
1196                                        }),
1197                                )
1198                            }
1199                            LogKind::Logs => {
1200                                let log_view = log_view.clone();
1201                                div().child(
1202                                    PopoverMenu::new("lsp-log-level-menu")
1203                                        .anchor(Anchor::TopLeft)
1204                                        .trigger(
1205                                            Button::new(
1206                                                "language_server_log_level_selector",
1207                                                "Log level",
1208                                            )
1209                                            .end_icon(
1210                                                Icon::new(IconName::ChevronDown)
1211                                                    .size(IconSize::Small)
1212                                                    .color(Color::Muted),
1213                                            ),
1214                                        )
1215                                        .menu({
1216                                            let log_view = log_view;
1217
1218                                            move |window, cx| {
1219                                                let id = log_view.read(cx).current_server_id?;
1220
1221                                                let log_level =
1222                                                    log_view.update(cx, |this, cx| {
1223                                                        this.log_store.update(cx, |this, _| {
1224                                                            Some(
1225                                                                this.get_language_server_state(id)?
1226                                                                    .log_level,
1227                                                            )
1228                                                        })
1229                                                    })?;
1230
1231                                                ContextMenu::build(
1232                                                    window,
1233                                                    cx,
1234                                                    |mut menu, window, cx| {
1235                                                        let log_view = log_view.clone();
1236
1237                                                        for (option, label) in [
1238                                                            (MessageType::LOG, "Log"),
1239                                                            (MessageType::INFO, "Info"),
1240                                                            (MessageType::WARNING, "Warning"),
1241                                                            (MessageType::ERROR, "Error"),
1242                                                        ] {
1243                                                            menu = menu.entry(label, None, {
1244                                                                let log_view = log_view.clone();
1245                                                                move |window, cx| {
1246                                                                    log_view.update(cx, |this, cx| {
1247                                                                    if let Some(id) =
1248                                                                        this.current_server_id
1249                                                                    {
1250                                                                        this.update_log_level(
1251                                                                            id, option, window, cx,
1252                                                                        );
1253                                                                    }
1254                                                                });
1255                                                                }
1256                                                            });
1257                                                            if option == log_level {
1258                                                                menu.select_last(window, cx);
1259                                                            }
1260                                                        }
1261
1262                                                        menu
1263                                                    },
1264                                                )
1265                                                .into()
1266                                            }
1267                                        }),
1268                                )
1269                            }
1270                            _ => div(),
1271                        }),
1272                    ),
1273            )
1274            .child(
1275                Button::new("clear_log_button", "Clear").on_click(cx.listener(
1276                    |this, _, window, cx| {
1277                        if let Some(log_view) = this.log_view.as_ref() {
1278                            log_view.update(cx, |log_view, cx| {
1279                                log_view.editor.update(cx, |editor, cx| {
1280                                    editor.set_read_only(false);
1281                                    editor.clear(window, cx);
1282                                    editor.set_read_only(true);
1283                                });
1284                            })
1285                        }
1286                    },
1287                )),
1288            )
1289    }
1290}
1291
1292fn initialize_new_editor(
1293    content: String,
1294    move_to_end: bool,
1295    window: &mut Window,
1296    cx: &mut App,
1297) -> Entity<Editor> {
1298    cx.new(|cx| {
1299        let mut editor = Editor::multi_line(window, cx);
1300        editor.hide_minimap_by_default(window, cx);
1301        editor.set_text(content, window, cx);
1302        editor.set_show_git_diff_gutter(false, cx);
1303        editor.set_show_runnables(false, cx);
1304        editor.set_show_bookmarks(false, cx);
1305        editor.set_show_breakpoints(false, cx);
1306        editor.set_read_only(true);
1307        editor.set_show_edit_predictions(Some(false), window, cx);
1308        editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1309        if move_to_end {
1310            editor.move_to_end(&MoveToEnd, window, cx);
1311        }
1312        editor
1313    })
1314}
1315
1316const RPC_MESSAGES: &str = "RPC Messages";
1317const SERVER_LOGS: &str = "Server Logs";
1318const SERVER_TRACE: &str = "Server Trace";
1319const SERVER_INFO: &str = "Server Info";
1320
1321impl LspLogToolbarItemView {
1322    pub fn new() -> Self {
1323        Self {
1324            log_view: None,
1325            _log_view_subscription: None,
1326        }
1327    }
1328
1329    fn toggle_rpc_logging_for_server(
1330        &mut self,
1331        id: LanguageServerId,
1332        enabled: bool,
1333        window: &mut Window,
1334        cx: &mut Context<Self>,
1335    ) {
1336        if let Some(log_view) = &self.log_view {
1337            log_view.update(cx, |log_view, cx| {
1338                log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1339                if !enabled && Some(id) == log_view.current_server_id {
1340                    log_view.show_logs_for_server(id, window, cx);
1341                    cx.notify();
1342                } else if enabled {
1343                    log_view.show_rpc_trace_for_server(id, window, cx);
1344                    cx.notify();
1345                }
1346                window.focus(&log_view.focus_handle, cx);
1347            });
1348        }
1349        cx.notify();
1350    }
1351}
1352
1353struct ServerInfo {
1354    id: LanguageServerId,
1355    capabilities: lsp::ServerCapabilities,
1356    status: LanguageServerStatus,
1357}
1358
1359impl ServerInfo {
1360    fn new(server: &LanguageServer) -> Self {
1361        Self {
1362            id: server.server_id(),
1363            capabilities: server.capabilities(),
1364            status: LanguageServerStatus {
1365                name: server.name(),
1366                language_name: None,
1367                server_version: server.version(),
1368                server_readable_version: server.readable_version(),
1369                pending_work: Default::default(),
1370                has_pending_diagnostic_updates: false,
1371                progress_tokens: Default::default(),
1372                worktree: None,
1373                binary: Some(server.binary().clone()),
1374                configuration: Some(server.configuration().clone()),
1375                workspace_folders: server.workspace_folders(),
1376                process_id: server.process_id(),
1377            },
1378        }
1379    }
1380}
1381
1382impl EventEmitter<EditorEvent> for LspLogView {}
1383impl EventEmitter<SearchEvent> for LspLogView {}
1384
Served at tenant.openagents/omega Member data and write actions are omitted.