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