lsp_log_view.rs

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