lsp_log.rs

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