lsp_log_view.rs

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