lsp_log.rs

   1use collections::{HashMap, VecDeque};
   2use editor::{Editor, MoveToEnd};
   3use futures::{channel::mpsc, StreamExt};
   4use gpui::{
   5    actions,
   6    elements::{
   7        AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
   8        ParentElement, Stack,
   9    },
  10    platform::{CursorStyle, MouseButton},
  11    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
  12    ViewContext, ViewHandle, WeakModelHandle,
  13};
  14use language::{LanguageServerId, LanguageServerName};
  15use lsp::IoKind;
  16use project::{search::SearchQuery, Project};
  17use std::{borrow::Cow, sync::Arc};
  18use theme::{ui, Theme};
  19use workspace::{
  20    item::{Item, ItemHandle},
  21    searchable::{SearchableItem, SearchableItemHandle},
  22    ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
  23};
  24
  25const SEND_LINE: &str = "// Send:";
  26const RECEIVE_LINE: &str = "// Receive:";
  27const MAX_STORED_LOG_ENTRIES: usize = 2000;
  28
  29pub struct LogStore {
  30    projects: HashMap<WeakModelHandle<Project>, ProjectState>,
  31    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, IoKind, String)>,
  32}
  33
  34struct ProjectState {
  35    servers: HashMap<LanguageServerId, LanguageServerState>,
  36    _subscriptions: [gpui::Subscription; 2],
  37}
  38
  39struct LanguageServerState {
  40    log_messages: VecDeque<String>,
  41    rpc_state: Option<LanguageServerRpcState>,
  42    _io_logs_subscription: Option<lsp::Subscription>,
  43    _lsp_logs_subscription: Option<lsp::Subscription>,
  44}
  45
  46struct LanguageServerRpcState {
  47    rpc_messages: VecDeque<String>,
  48    last_message_kind: Option<MessageKind>,
  49}
  50
  51pub struct LspLogView {
  52    pub(crate) editor: ViewHandle<Editor>,
  53    editor_subscription: Subscription,
  54    log_store: ModelHandle<LogStore>,
  55    current_server_id: Option<LanguageServerId>,
  56    is_showing_rpc_trace: bool,
  57    project: ModelHandle<Project>,
  58    _log_store_subscriptions: Vec<Subscription>,
  59}
  60
  61pub struct LspLogToolbarItemView {
  62    log_view: Option<ViewHandle<LspLogView>>,
  63    _log_view_subscription: Option<Subscription>,
  64    menu_open: bool,
  65}
  66
  67#[derive(Copy, Clone, PartialEq, Eq)]
  68enum MessageKind {
  69    Send,
  70    Receive,
  71}
  72
  73#[derive(Clone, Debug, PartialEq)]
  74pub(crate) struct LogMenuItem {
  75    pub server_id: LanguageServerId,
  76    pub server_name: LanguageServerName,
  77    pub worktree_root_name: String,
  78    pub rpc_trace_enabled: bool,
  79    pub rpc_trace_selected: bool,
  80    pub logs_selected: bool,
  81}
  82
  83actions!(debug, [OpenLanguageServerLogs]);
  84
  85pub fn init(cx: &mut AppContext) {
  86    let log_store = cx.add_model(|cx| LogStore::new(cx));
  87
  88    cx.subscribe_global::<WorkspaceCreated, _>({
  89        let log_store = log_store.clone();
  90        move |event, cx| {
  91            let workspace = &event.0;
  92            if let Some(workspace) = workspace.upgrade(cx) {
  93                let project = workspace.read(cx).project().clone();
  94                if project.read(cx).is_local() {
  95                    log_store.update(cx, |store, cx| {
  96                        store.add_project(&project, cx);
  97                    });
  98                }
  99            }
 100        }
 101    })
 102    .detach();
 103
 104    cx.add_action(
 105        move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
 106            let project = workspace.project().read(cx);
 107            if project.is_local() {
 108                workspace.add_item(
 109                    Box::new(cx.add_view(|cx| {
 110                        LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
 111                    })),
 112                    cx,
 113                );
 114            }
 115        },
 116    );
 117}
 118
 119impl LogStore {
 120    pub fn new(cx: &mut ModelContext<Self>) -> Self {
 121        let (io_tx, mut io_rx) = mpsc::unbounded();
 122        let this = Self {
 123            projects: HashMap::default(),
 124            io_tx,
 125        };
 126        cx.spawn_weak(|this, mut cx| async move {
 127            while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
 128                if let Some(this) = this.upgrade(&cx) {
 129                    this.update(&mut cx, |this, cx| {
 130                        this.on_io(project, server_id, io_kind, &message, cx);
 131                    });
 132                }
 133            }
 134            anyhow::Ok(())
 135        })
 136        .detach();
 137        this
 138    }
 139
 140    pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
 141        let weak_project = project.downgrade();
 142        self.projects.insert(
 143            weak_project,
 144            ProjectState {
 145                servers: HashMap::default(),
 146                _subscriptions: [
 147                    cx.observe_release(&project, move |this, _, _| {
 148                        this.projects.remove(&weak_project);
 149                    }),
 150                    cx.subscribe(project, |this, project, event, cx| match event {
 151                        project::Event::LanguageServerAdded(id) => {
 152                            this.add_language_server(&project, *id, cx);
 153                        }
 154                        project::Event::LanguageServerRemoved(id) => {
 155                            this.remove_language_server(&project, *id, cx);
 156                        }
 157                        project::Event::LanguageServerLog(id, message) => {
 158                            this.add_language_server_log(&project, *id, message, cx);
 159                        }
 160                        _ => {}
 161                    }),
 162                ],
 163            },
 164        );
 165    }
 166
 167    fn add_language_server(
 168        &mut self,
 169        project: &ModelHandle<Project>,
 170        id: LanguageServerId,
 171        cx: &mut ModelContext<Self>,
 172    ) -> Option<&mut LanguageServerState> {
 173        let project_state = self.projects.get_mut(&project.downgrade())?;
 174        let server_state = project_state.servers.entry(id).or_insert_with(|| {
 175            cx.notify();
 176            LanguageServerState {
 177                rpc_state: None,
 178                log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
 179                _io_logs_subscription: None,
 180                _lsp_logs_subscription: None,
 181            }
 182        });
 183
 184        let server = project.read(cx).language_server_for_id(id);
 185        if let Some(server) = server.as_deref() {
 186            if server.has_notification_handler::<lsp::notification::LogMessage>() {
 187                // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
 188                return Some(server_state);
 189            }
 190        }
 191
 192        let weak_project = project.downgrade();
 193        let io_tx = self.io_tx.clone();
 194        server_state._io_logs_subscription = server.as_ref().map(|server| {
 195            server.on_io(move |io_kind, message| {
 196                io_tx
 197                    .unbounded_send((weak_project, id, io_kind, message.to_string()))
 198                    .ok();
 199            })
 200        });
 201        let this = cx.weak_handle();
 202        let weak_project = project.downgrade();
 203        server_state._lsp_logs_subscription = server.map(|server| {
 204            let server_id = server.server_id();
 205            server.on_notification::<lsp::notification::LogMessage, _>({
 206                move |params, mut cx| {
 207                    if let Some((project, this)) =
 208                        weak_project.upgrade(&mut cx).zip(this.upgrade(&mut cx))
 209                    {
 210                        this.update(&mut cx, |this, cx| {
 211                            this.add_language_server_log(&project, server_id, &params.message, cx);
 212                        });
 213                    }
 214                }
 215            })
 216        });
 217        Some(server_state)
 218    }
 219
 220    fn add_language_server_log(
 221        &mut self,
 222        project: &ModelHandle<Project>,
 223        id: LanguageServerId,
 224        message: &str,
 225        cx: &mut ModelContext<Self>,
 226    ) -> Option<()> {
 227        let language_server_state = match self
 228            .projects
 229            .get_mut(&project.downgrade())?
 230            .servers
 231            .get_mut(&id)
 232        {
 233            Some(existing_state) => existing_state,
 234            None => self.add_language_server(&project, id, cx)?,
 235        };
 236
 237        let log_lines = &mut language_server_state.log_messages;
 238        while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
 239            log_lines.pop_front();
 240        }
 241        let message = message.trim();
 242        log_lines.push_back(message.to_string());
 243        cx.emit(Event::NewServerLogEntry {
 244            id,
 245            entry: message.to_string(),
 246            is_rpc: false,
 247        });
 248        cx.notify();
 249        Some(())
 250    }
 251
 252    fn remove_language_server(
 253        &mut self,
 254        project: &ModelHandle<Project>,
 255        id: LanguageServerId,
 256        cx: &mut ModelContext<Self>,
 257    ) -> Option<()> {
 258        let project_state = self.projects.get_mut(&project.downgrade())?;
 259        project_state.servers.remove(&id);
 260        cx.notify();
 261        Some(())
 262    }
 263
 264    fn server_logs(
 265        &self,
 266        project: &ModelHandle<Project>,
 267        server_id: LanguageServerId,
 268    ) -> Option<&VecDeque<String>> {
 269        let weak_project = project.downgrade();
 270        let project_state = self.projects.get(&weak_project)?;
 271        let server_state = project_state.servers.get(&server_id)?;
 272        Some(&server_state.log_messages)
 273    }
 274
 275    fn enable_rpc_trace_for_language_server(
 276        &mut self,
 277        project: &ModelHandle<Project>,
 278        server_id: LanguageServerId,
 279    ) -> Option<&mut LanguageServerRpcState> {
 280        let weak_project = project.downgrade();
 281        let project_state = self.projects.get_mut(&weak_project)?;
 282        let server_state = project_state.servers.get_mut(&server_id)?;
 283        let rpc_state = server_state
 284            .rpc_state
 285            .get_or_insert_with(|| LanguageServerRpcState {
 286                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
 287                last_message_kind: None,
 288            });
 289        Some(rpc_state)
 290    }
 291
 292    pub fn disable_rpc_trace_for_language_server(
 293        &mut self,
 294        project: &ModelHandle<Project>,
 295        server_id: LanguageServerId,
 296        _: &mut ModelContext<Self>,
 297    ) -> Option<()> {
 298        let project = project.downgrade();
 299        let project_state = self.projects.get_mut(&project)?;
 300        let server_state = project_state.servers.get_mut(&server_id)?;
 301        server_state.rpc_state.take();
 302        Some(())
 303    }
 304
 305    fn on_io(
 306        &mut self,
 307        project: WeakModelHandle<Project>,
 308        language_server_id: LanguageServerId,
 309        io_kind: IoKind,
 310        message: &str,
 311        cx: &mut ModelContext<Self>,
 312    ) -> Option<()> {
 313        let is_received = match io_kind {
 314            IoKind::StdOut => true,
 315            IoKind::StdIn => false,
 316            IoKind::StdErr => {
 317                let project = project.upgrade(cx)?;
 318                let message = format!("stderr: {}", message.trim());
 319                self.add_language_server_log(&project, language_server_id, &message, cx);
 320                return Some(());
 321            }
 322        };
 323
 324        let state = self
 325            .projects
 326            .get_mut(&project)?
 327            .servers
 328            .get_mut(&language_server_id)?
 329            .rpc_state
 330            .as_mut()?;
 331        let kind = if is_received {
 332            MessageKind::Receive
 333        } else {
 334            MessageKind::Send
 335        };
 336
 337        let rpc_log_lines = &mut state.rpc_messages;
 338        if state.last_message_kind != Some(kind) {
 339            let line_before_message = match kind {
 340                MessageKind::Send => SEND_LINE,
 341                MessageKind::Receive => RECEIVE_LINE,
 342            };
 343            rpc_log_lines.push_back(line_before_message.to_string());
 344            cx.emit(Event::NewServerLogEntry {
 345                id: language_server_id,
 346                entry: line_before_message.to_string(),
 347                is_rpc: true,
 348            });
 349        }
 350
 351        while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
 352            rpc_log_lines.pop_front();
 353        }
 354        let message = message.trim();
 355        rpc_log_lines.push_back(message.to_string());
 356        cx.emit(Event::NewServerLogEntry {
 357            id: language_server_id,
 358            entry: message.to_string(),
 359            is_rpc: true,
 360        });
 361        cx.notify();
 362        Some(())
 363    }
 364}
 365
 366impl LspLogView {
 367    pub fn new(
 368        project: ModelHandle<Project>,
 369        log_store: ModelHandle<LogStore>,
 370        cx: &mut ViewContext<Self>,
 371    ) -> Self {
 372        let server_id = log_store
 373            .read(cx)
 374            .projects
 375            .get(&project.downgrade())
 376            .and_then(|project| project.servers.keys().copied().next());
 377        let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
 378            (|| -> Option<()> {
 379                let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
 380                if let Some(current_lsp) = this.current_server_id {
 381                    if !project_state.servers.contains_key(&current_lsp) {
 382                        if let Some(server) = project_state.servers.iter().next() {
 383                            if this.is_showing_rpc_trace {
 384                                this.show_rpc_trace_for_server(*server.0, cx)
 385                            } else {
 386                                this.show_logs_for_server(*server.0, cx)
 387                            }
 388                        } else {
 389                            this.current_server_id = None;
 390                            this.editor.update(cx, |editor, cx| {
 391                                editor.set_read_only(false);
 392                                editor.clear(cx);
 393                                editor.set_read_only(true);
 394                            });
 395                            cx.notify();
 396                        }
 397                    }
 398                } else {
 399                    if let Some(server) = project_state.servers.iter().next() {
 400                        if this.is_showing_rpc_trace {
 401                            this.show_rpc_trace_for_server(*server.0, cx)
 402                        } else {
 403                            this.show_logs_for_server(*server.0, cx)
 404                        }
 405                    }
 406                }
 407
 408                Some(())
 409            })();
 410
 411            cx.notify();
 412        });
 413        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
 414            Event::NewServerLogEntry { id, entry, is_rpc } => {
 415                if log_view.current_server_id == Some(*id) {
 416                    if (*is_rpc && log_view.is_showing_rpc_trace)
 417                        || (!*is_rpc && !log_view.is_showing_rpc_trace)
 418                    {
 419                        log_view.editor.update(cx, |editor, cx| {
 420                            editor.set_read_only(false);
 421                            editor.handle_input(entry.trim(), cx);
 422                            editor.handle_input("\n", cx);
 423                            editor.set_read_only(true);
 424                        });
 425                    }
 426                }
 427            }
 428        });
 429        let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
 430        let mut this = Self {
 431            editor,
 432            editor_subscription,
 433            project,
 434            log_store,
 435            current_server_id: None,
 436            is_showing_rpc_trace: false,
 437            _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
 438        };
 439        if let Some(server_id) = server_id {
 440            this.show_logs_for_server(server_id, cx);
 441        }
 442        this
 443    }
 444
 445    fn editor_for_logs(
 446        log_contents: String,
 447        cx: &mut ViewContext<Self>,
 448    ) -> (ViewHandle<Editor>, Subscription) {
 449        let editor = cx.add_view(|cx| {
 450            let mut editor = Editor::multi_line(None, cx);
 451            editor.set_text(log_contents, cx);
 452            editor.move_to_end(&MoveToEnd, cx);
 453            editor.set_read_only(true);
 454            editor
 455        });
 456        let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
 457        (editor, editor_subscription)
 458    }
 459
 460    pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
 461        let log_store = self.log_store.read(cx);
 462        let state = log_store.projects.get(&self.project.downgrade())?;
 463        let mut rows = self
 464            .project
 465            .read(cx)
 466            .language_servers()
 467            .filter_map(|(server_id, language_server_name, worktree_id)| {
 468                let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
 469                let state = state.servers.get(&server_id)?;
 470                Some(LogMenuItem {
 471                    server_id,
 472                    server_name: language_server_name,
 473                    worktree_root_name: worktree.read(cx).root_name().to_string(),
 474                    rpc_trace_enabled: state.rpc_state.is_some(),
 475                    rpc_trace_selected: self.is_showing_rpc_trace
 476                        && self.current_server_id == Some(server_id),
 477                    logs_selected: !self.is_showing_rpc_trace
 478                        && self.current_server_id == Some(server_id),
 479                })
 480            })
 481            .chain(
 482                self.project
 483                    .read(cx)
 484                    .supplementary_language_servers()
 485                    .filter_map(|(&server_id, (name, _))| {
 486                        let state = state.servers.get(&server_id)?;
 487                        Some(LogMenuItem {
 488                            server_id,
 489                            server_name: name.clone(),
 490                            worktree_root_name: "supplementary".to_string(),
 491                            rpc_trace_enabled: state.rpc_state.is_some(),
 492                            rpc_trace_selected: self.is_showing_rpc_trace
 493                                && self.current_server_id == Some(server_id),
 494                            logs_selected: !self.is_showing_rpc_trace
 495                                && self.current_server_id == Some(server_id),
 496                        })
 497                    }),
 498            )
 499            .collect::<Vec<_>>();
 500        rows.sort_by_key(|row| row.server_id);
 501        rows.dedup_by_key(|row| row.server_id);
 502        Some(rows)
 503    }
 504
 505    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
 506        let log_contents = self
 507            .log_store
 508            .read(cx)
 509            .server_logs(&self.project, server_id)
 510            .map(log_contents);
 511        if let Some(log_contents) = log_contents {
 512            self.current_server_id = Some(server_id);
 513            self.is_showing_rpc_trace = false;
 514            let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
 515            self.editor = editor;
 516            self.editor_subscription = editor_subscription;
 517            cx.notify();
 518        }
 519    }
 520
 521    fn show_rpc_trace_for_server(
 522        &mut self,
 523        server_id: LanguageServerId,
 524        cx: &mut ViewContext<Self>,
 525    ) {
 526        let rpc_log = self.log_store.update(cx, |log_store, _| {
 527            log_store
 528                .enable_rpc_trace_for_language_server(&self.project, server_id)
 529                .map(|state| log_contents(&state.rpc_messages))
 530        });
 531        if let Some(rpc_log) = rpc_log {
 532            self.current_server_id = Some(server_id);
 533            self.is_showing_rpc_trace = true;
 534            let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
 535            let language = self.project.read(cx).languages().language_for_name("JSON");
 536            editor
 537                .read(cx)
 538                .buffer()
 539                .read(cx)
 540                .as_singleton()
 541                .expect("log buffer should be a singleton")
 542                .update(cx, |_, cx| {
 543                    cx.spawn_weak({
 544                        let buffer = cx.handle();
 545                        |_, mut cx| async move {
 546                            let language = language.await.ok();
 547                            buffer.update(&mut cx, |buffer, cx| {
 548                                buffer.set_language(language, cx);
 549                            });
 550                        }
 551                    })
 552                    .detach();
 553                });
 554
 555            self.editor = editor;
 556            self.editor_subscription = editor_subscription;
 557            cx.notify();
 558        }
 559    }
 560
 561    fn toggle_rpc_trace_for_server(
 562        &mut self,
 563        server_id: LanguageServerId,
 564        enabled: bool,
 565        cx: &mut ViewContext<Self>,
 566    ) {
 567        self.log_store.update(cx, |log_store, cx| {
 568            if enabled {
 569                log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
 570            } else {
 571                log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
 572            }
 573        });
 574        if !enabled && Some(server_id) == self.current_server_id {
 575            self.show_logs_for_server(server_id, cx);
 576            cx.notify();
 577        }
 578    }
 579}
 580
 581fn log_contents(lines: &VecDeque<String>) -> String {
 582    let (a, b) = lines.as_slices();
 583    let log_contents = a.join("\n");
 584    if b.is_empty() {
 585        log_contents
 586    } else {
 587        log_contents + "\n" + &b.join("\n")
 588    }
 589}
 590
 591impl View for LspLogView {
 592    fn ui_name() -> &'static str {
 593        "LspLogView"
 594    }
 595
 596    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 597        ChildView::new(&self.editor, cx).into_any()
 598    }
 599
 600    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
 601        if cx.is_self_focused() {
 602            cx.focus(&self.editor);
 603        }
 604    }
 605}
 606
 607impl Item for LspLogView {
 608    fn tab_content<V: 'static>(
 609        &self,
 610        _: Option<usize>,
 611        style: &theme::Tab,
 612        _: &AppContext,
 613    ) -> AnyElement<V> {
 614        Label::new("LSP Logs", style.label.clone()).into_any()
 615    }
 616
 617    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 618        Some(Box::new(handle.clone()))
 619    }
 620}
 621
 622impl SearchableItem for LspLogView {
 623    type Match = <Editor as SearchableItem>::Match;
 624
 625    fn to_search_event(
 626        &mut self,
 627        event: &Self::Event,
 628        cx: &mut ViewContext<Self>,
 629    ) -> Option<workspace::searchable::SearchEvent> {
 630        self.editor
 631            .update(cx, |editor, cx| editor.to_search_event(event, cx))
 632    }
 633
 634    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 635        self.editor.update(cx, |e, cx| e.clear_matches(cx))
 636    }
 637
 638    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
 639        self.editor
 640            .update(cx, |e, cx| e.update_matches(matches, cx))
 641    }
 642
 643    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
 644        self.editor.update(cx, |e, cx| e.query_suggestion(cx))
 645    }
 646
 647    fn activate_match(
 648        &mut self,
 649        index: usize,
 650        matches: Vec<Self::Match>,
 651        cx: &mut ViewContext<Self>,
 652    ) {
 653        self.editor
 654            .update(cx, |e, cx| e.activate_match(index, matches, cx))
 655    }
 656
 657    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
 658        self.editor
 659            .update(cx, |e, cx| e.select_matches(matches, cx))
 660    }
 661
 662    fn find_matches(
 663        &mut self,
 664        query: Arc<project::search::SearchQuery>,
 665        cx: &mut ViewContext<Self>,
 666    ) -> gpui::Task<Vec<Self::Match>> {
 667        self.editor.update(cx, |e, cx| e.find_matches(query, cx))
 668    }
 669
 670    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
 671        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
 672    }
 673    fn supported_options() -> workspace::searchable::SearchOptions {
 674        workspace::searchable::SearchOptions {
 675            case: true,
 676            word: true,
 677            regex: true,
 678            // LSP log is read-only.
 679            replacement: false,
 680        }
 681    }
 682    fn active_match_index(
 683        &mut self,
 684        matches: Vec<Self::Match>,
 685        cx: &mut ViewContext<Self>,
 686    ) -> Option<usize> {
 687        self.editor
 688            .update(cx, |e, cx| e.active_match_index(matches, cx))
 689    }
 690}
 691
 692impl ToolbarItemView for LspLogToolbarItemView {
 693    fn set_active_pane_item(
 694        &mut self,
 695        active_pane_item: Option<&dyn ItemHandle>,
 696        cx: &mut ViewContext<Self>,
 697    ) -> workspace::ToolbarItemLocation {
 698        self.menu_open = false;
 699        if let Some(item) = active_pane_item {
 700            if let Some(log_view) = item.downcast::<LspLogView>() {
 701                self.log_view = Some(log_view.clone());
 702                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
 703                    cx.notify();
 704                }));
 705                return ToolbarItemLocation::PrimaryLeft {
 706                    flex: Some((1., false)),
 707                };
 708            }
 709        }
 710        self.log_view = None;
 711        self._log_view_subscription = None;
 712        ToolbarItemLocation::Hidden
 713    }
 714}
 715
 716impl View for LspLogToolbarItemView {
 717    fn ui_name() -> &'static str {
 718        "LspLogView"
 719    }
 720
 721    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 722        let theme = theme::current(cx).clone();
 723        let Some(log_view) = self.log_view.as_ref() else {
 724            return Empty::new().into_any();
 725        };
 726        let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
 727            let menu_rows = log_view.menu_items(cx).unwrap_or_default();
 728            let current_server_id = log_view.current_server_id;
 729            (menu_rows, current_server_id)
 730        });
 731
 732        let current_server = current_server_id.and_then(|current_server_id| {
 733            if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
 734                Some(menu_rows[ix].clone())
 735            } else {
 736                None
 737            }
 738        });
 739        let server_selected = current_server.is_some();
 740
 741        enum LspLogScroll {}
 742        enum Menu {}
 743        let lsp_menu = Stack::new()
 744            .with_child(Self::render_language_server_menu_header(
 745                current_server,
 746                &theme,
 747                cx,
 748            ))
 749            .with_children(if self.menu_open {
 750                Some(
 751                    Overlay::new(
 752                        MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
 753                            Flex::column()
 754                                .scrollable::<LspLogScroll>(0, None, cx)
 755                                .with_children(menu_rows.into_iter().map(|row| {
 756                                    Self::render_language_server_menu_item(
 757                                        row.server_id,
 758                                        row.server_name,
 759                                        &row.worktree_root_name,
 760                                        row.rpc_trace_enabled,
 761                                        row.logs_selected,
 762                                        row.rpc_trace_selected,
 763                                        &theme,
 764                                        cx,
 765                                    )
 766                                }))
 767                                .contained()
 768                                .with_style(theme.toolbar_dropdown_menu.container)
 769                                .constrained()
 770                                .with_width(400.)
 771                                .with_height(400.)
 772                        })
 773                        .on_down_out(MouseButton::Left, |_, this, cx| {
 774                            this.menu_open = false;
 775                            cx.notify()
 776                        }),
 777                    )
 778                    .with_hoverable(true)
 779                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
 780                    .with_anchor_corner(AnchorCorner::TopLeft)
 781                    .with_z_index(999)
 782                    .aligned()
 783                    .bottom()
 784                    .left(),
 785                )
 786            } else {
 787                None
 788            })
 789            .aligned()
 790            .left()
 791            .clipped();
 792
 793        enum LspCleanupButton {}
 794        let log_cleanup_button =
 795            MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
 796                let theme = theme::current(cx).clone();
 797                let style = theme
 798                    .workspace
 799                    .toolbar
 800                    .toggleable_text_tool
 801                    .in_state(server_selected)
 802                    .style_for(state);
 803                Label::new("Clear", style.text.clone())
 804                    .aligned()
 805                    .contained()
 806                    .with_style(style.container)
 807                    .constrained()
 808                    .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
 809            })
 810            .on_click(MouseButton::Left, move |_, this, cx| {
 811                if let Some(log_view) = this.log_view.as_ref() {
 812                    log_view.update(cx, |log_view, cx| {
 813                        log_view.editor.update(cx, |editor, cx| {
 814                            editor.set_read_only(false);
 815                            editor.clear(cx);
 816                            editor.set_read_only(true);
 817                        });
 818                    })
 819                }
 820            })
 821            .with_cursor_style(CursorStyle::PointingHand)
 822            .aligned()
 823            .right();
 824
 825        Flex::row()
 826            .with_child(lsp_menu)
 827            .with_child(log_cleanup_button)
 828            .contained()
 829            .aligned()
 830            .left()
 831            .into_any_named("lsp log controls")
 832    }
 833}
 834
 835const RPC_MESSAGES: &str = "RPC Messages";
 836const SERVER_LOGS: &str = "Server Logs";
 837
 838impl LspLogToolbarItemView {
 839    pub fn new() -> Self {
 840        Self {
 841            menu_open: false,
 842            log_view: None,
 843            _log_view_subscription: None,
 844        }
 845    }
 846
 847    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
 848        self.menu_open = !self.menu_open;
 849        cx.notify();
 850    }
 851
 852    fn toggle_logging_for_server(
 853        &mut self,
 854        id: LanguageServerId,
 855        enabled: bool,
 856        cx: &mut ViewContext<Self>,
 857    ) {
 858        if let Some(log_view) = &self.log_view {
 859            log_view.update(cx, |log_view, cx| {
 860                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
 861                if !enabled && Some(id) == log_view.current_server_id {
 862                    log_view.show_logs_for_server(id, cx);
 863                    cx.notify();
 864                }
 865            });
 866        }
 867        cx.notify();
 868    }
 869
 870    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
 871        if let Some(log_view) = &self.log_view {
 872            log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
 873            self.menu_open = false;
 874            cx.notify();
 875        }
 876    }
 877
 878    fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
 879        if let Some(log_view) = &self.log_view {
 880            log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
 881            self.menu_open = false;
 882            cx.notify();
 883        }
 884    }
 885
 886    fn render_language_server_menu_header(
 887        current_server: Option<LogMenuItem>,
 888        theme: &Arc<Theme>,
 889        cx: &mut ViewContext<Self>,
 890    ) -> impl Element<Self> {
 891        enum ToggleMenu {}
 892        MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
 893            let label: Cow<str> = current_server
 894                .and_then(|row| {
 895                    Some(
 896                        format!(
 897                            "{} ({}) - {}",
 898                            row.server_name.0,
 899                            row.worktree_root_name,
 900                            if row.rpc_trace_selected {
 901                                RPC_MESSAGES
 902                            } else {
 903                                SERVER_LOGS
 904                            },
 905                        )
 906                        .into(),
 907                    )
 908                })
 909                .unwrap_or_else(|| "No server selected".into());
 910            let style = theme.toolbar_dropdown_menu.header.style_for(state);
 911            Label::new(label, style.text.clone())
 912                .contained()
 913                .with_style(style.container)
 914        })
 915        .with_cursor_style(CursorStyle::PointingHand)
 916        .on_click(MouseButton::Left, move |_, view, cx| {
 917            view.toggle_menu(cx);
 918        })
 919    }
 920
 921    fn render_language_server_menu_item(
 922        id: LanguageServerId,
 923        name: LanguageServerName,
 924        worktree_root_name: &str,
 925        rpc_trace_enabled: bool,
 926        logs_selected: bool,
 927        rpc_trace_selected: bool,
 928        theme: &Arc<Theme>,
 929        cx: &mut ViewContext<Self>,
 930    ) -> impl Element<Self> {
 931        enum ActivateLog {}
 932        enum ActivateRpcTrace {}
 933        enum LanguageServerCheckbox {}
 934
 935        Flex::column()
 936            .with_child({
 937                let style = &theme.toolbar_dropdown_menu.section_header;
 938                Label::new(
 939                    format!("{} ({})", name.0, worktree_root_name),
 940                    style.text.clone(),
 941                )
 942                .contained()
 943                .with_style(style.container)
 944                .constrained()
 945                .with_height(theme.toolbar_dropdown_menu.row_height)
 946            })
 947            .with_child(
 948                MouseEventHandler::new::<ActivateLog, _>(id.0, cx, move |state, _| {
 949                    let style = theme
 950                        .toolbar_dropdown_menu
 951                        .item
 952                        .in_state(logs_selected)
 953                        .style_for(state);
 954                    Label::new(SERVER_LOGS, style.text.clone())
 955                        .contained()
 956                        .with_style(style.container)
 957                        .constrained()
 958                        .with_height(theme.toolbar_dropdown_menu.row_height)
 959                })
 960                .with_cursor_style(CursorStyle::PointingHand)
 961                .on_click(MouseButton::Left, move |_, view, cx| {
 962                    view.show_logs_for_server(id, cx);
 963                }),
 964            )
 965            .with_child(
 966                MouseEventHandler::new::<ActivateRpcTrace, _>(id.0, cx, move |state, cx| {
 967                    let style = theme
 968                        .toolbar_dropdown_menu
 969                        .item
 970                        .in_state(rpc_trace_selected)
 971                        .style_for(state);
 972                    Flex::row()
 973                        .with_child(
 974                            Label::new(RPC_MESSAGES, style.text.clone())
 975                                .constrained()
 976                                .with_height(theme.toolbar_dropdown_menu.row_height),
 977                        )
 978                        .with_child(
 979                            ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
 980                                Empty::new(),
 981                                &theme.welcome.checkbox,
 982                                rpc_trace_enabled,
 983                                id.0,
 984                                cx,
 985                                move |this, enabled, cx| {
 986                                    this.toggle_logging_for_server(id, enabled, cx);
 987                                },
 988                            )
 989                            .flex_float(),
 990                        )
 991                        .align_children_center()
 992                        .contained()
 993                        .with_style(style.container)
 994                        .constrained()
 995                        .with_height(theme.toolbar_dropdown_menu.row_height)
 996                })
 997                .with_cursor_style(CursorStyle::PointingHand)
 998                .on_click(MouseButton::Left, move |_, view, cx| {
 999                    view.show_rpc_trace_for_server(id, cx);
1000                }),
1001            )
1002    }
1003}
1004
1005pub enum Event {
1006    NewServerLogEntry {
1007        id: LanguageServerId,
1008        entry: String,
1009        is_rpc: bool,
1010    },
1011}
1012
1013impl Entity for LogStore {
1014    type Event = Event;
1015}
1016
1017impl Entity for LspLogView {
1018    type Event = editor::Event;
1019}
1020
1021impl Entity for LspLogToolbarItemView {
1022    type Event = ();
1023}