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