lsp_log_view.rs

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