lsp_log_view.rs

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