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