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 Menu {}
 742        let lsp_menu = Stack::new()
 743            .with_child(Self::render_language_server_menu_header(
 744                current_server,
 745                &theme,
 746                cx,
 747            ))
 748            .with_children(if self.menu_open {
 749                Some(
 750                    Overlay::new(
 751                        MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
 752                            Flex::column()
 753                                .scrollable::<Self>(0, None, cx)
 754                                .with_children(menu_rows.into_iter().map(|row| {
 755                                    Self::render_language_server_menu_item(
 756                                        row.server_id,
 757                                        row.server_name,
 758                                        &row.worktree_root_name,
 759                                        row.rpc_trace_enabled,
 760                                        row.logs_selected,
 761                                        row.rpc_trace_selected,
 762                                        &theme,
 763                                        cx,
 764                                    )
 765                                }))
 766                                .contained()
 767                                .with_style(theme.toolbar_dropdown_menu.container)
 768                                .constrained()
 769                                .with_width(400.)
 770                                .with_height(400.)
 771                        })
 772                        .on_down_out(MouseButton::Left, |_, this, cx| {
 773                            this.menu_open = false;
 774                            cx.notify()
 775                        }),
 776                    )
 777                    .with_hoverable(true)
 778                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
 779                    .with_anchor_corner(AnchorCorner::TopLeft)
 780                    .with_z_index(999)
 781                    .aligned()
 782                    .bottom()
 783                    .left(),
 784                )
 785            } else {
 786                None
 787            })
 788            .aligned()
 789            .left()
 790            .clipped();
 791
 792        enum LspCleanupButton {}
 793        let log_cleanup_button =
 794            MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
 795                let theme = theme::current(cx).clone();
 796                let style = theme
 797                    .workspace
 798                    .toolbar
 799                    .toggleable_text_tool
 800                    .in_state(server_selected)
 801                    .style_for(state);
 802                Label::new("Clear", style.text.clone())
 803                    .aligned()
 804                    .contained()
 805                    .with_style(style.container)
 806                    .constrained()
 807                    .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
 808            })
 809            .on_click(MouseButton::Left, move |_, this, cx| {
 810                if let Some(log_view) = this.log_view.as_ref() {
 811                    log_view.update(cx, |log_view, cx| {
 812                        log_view.editor.update(cx, |editor, cx| {
 813                            editor.set_read_only(false);
 814                            editor.clear(cx);
 815                            editor.set_read_only(true);
 816                        });
 817                    })
 818                }
 819            })
 820            .with_cursor_style(CursorStyle::PointingHand)
 821            .aligned()
 822            .right();
 823
 824        Flex::row()
 825            .with_child(lsp_menu)
 826            .with_child(log_cleanup_button)
 827            .contained()
 828            .aligned()
 829            .left()
 830            .into_any_named("lsp log controls")
 831    }
 832}
 833
 834const RPC_MESSAGES: &str = "RPC Messages";
 835const SERVER_LOGS: &str = "Server Logs";
 836
 837impl LspLogToolbarItemView {
 838    pub fn new() -> Self {
 839        Self {
 840            menu_open: false,
 841            log_view: None,
 842            _log_view_subscription: None,
 843        }
 844    }
 845
 846    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
 847        self.menu_open = !self.menu_open;
 848        cx.notify();
 849    }
 850
 851    fn toggle_logging_for_server(
 852        &mut self,
 853        id: LanguageServerId,
 854        enabled: bool,
 855        cx: &mut ViewContext<Self>,
 856    ) {
 857        if let Some(log_view) = &self.log_view {
 858            log_view.update(cx, |log_view, cx| {
 859                log_view.toggle_rpc_trace_for_server(id, enabled, cx);
 860                if !enabled && Some(id) == log_view.current_server_id {
 861                    log_view.show_logs_for_server(id, cx);
 862                    cx.notify();
 863                }
 864            });
 865        }
 866        cx.notify();
 867    }
 868
 869    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
 870        if let Some(log_view) = &self.log_view {
 871            log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
 872            self.menu_open = false;
 873            cx.notify();
 874        }
 875    }
 876
 877    fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
 878        if let Some(log_view) = &self.log_view {
 879            log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
 880            self.menu_open = false;
 881            cx.notify();
 882        }
 883    }
 884
 885    fn render_language_server_menu_header(
 886        current_server: Option<LogMenuItem>,
 887        theme: &Arc<Theme>,
 888        cx: &mut ViewContext<Self>,
 889    ) -> impl Element<Self> {
 890        enum ToggleMenu {}
 891        MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
 892            let label: Cow<str> = current_server
 893                .and_then(|row| {
 894                    Some(
 895                        format!(
 896                            "{} ({}) - {}",
 897                            row.server_name.0,
 898                            row.worktree_root_name,
 899                            if row.rpc_trace_selected {
 900                                RPC_MESSAGES
 901                            } else {
 902                                SERVER_LOGS
 903                            },
 904                        )
 905                        .into(),
 906                    )
 907                })
 908                .unwrap_or_else(|| "No server selected".into());
 909            let style = theme.toolbar_dropdown_menu.header.style_for(state);
 910            Label::new(label, style.text.clone())
 911                .contained()
 912                .with_style(style.container)
 913        })
 914        .with_cursor_style(CursorStyle::PointingHand)
 915        .on_click(MouseButton::Left, move |_, view, cx| {
 916            view.toggle_menu(cx);
 917        })
 918    }
 919
 920    fn render_language_server_menu_item(
 921        id: LanguageServerId,
 922        name: LanguageServerName,
 923        worktree_root_name: &str,
 924        rpc_trace_enabled: bool,
 925        logs_selected: bool,
 926        rpc_trace_selected: bool,
 927        theme: &Arc<Theme>,
 928        cx: &mut ViewContext<Self>,
 929    ) -> impl Element<Self> {
 930        enum ActivateLog {}
 931        enum ActivateRpcTrace {}
 932
 933        Flex::column()
 934            .with_child({
 935                let style = &theme.toolbar_dropdown_menu.section_header;
 936                Label::new(
 937                    format!("{} ({})", name.0, worktree_root_name),
 938                    style.text.clone(),
 939                )
 940                .contained()
 941                .with_style(style.container)
 942                .constrained()
 943                .with_height(theme.toolbar_dropdown_menu.row_height)
 944            })
 945            .with_child(
 946                MouseEventHandler::new::<ActivateLog, _>(id.0, cx, move |state, _| {
 947                    let style = theme
 948                        .toolbar_dropdown_menu
 949                        .item
 950                        .in_state(logs_selected)
 951                        .style_for(state);
 952                    Label::new(SERVER_LOGS, style.text.clone())
 953                        .contained()
 954                        .with_style(style.container)
 955                        .constrained()
 956                        .with_height(theme.toolbar_dropdown_menu.row_height)
 957                })
 958                .with_cursor_style(CursorStyle::PointingHand)
 959                .on_click(MouseButton::Left, move |_, view, cx| {
 960                    view.show_logs_for_server(id, cx);
 961                }),
 962            )
 963            .with_child(
 964                MouseEventHandler::new::<ActivateRpcTrace, _>(id.0, cx, move |state, cx| {
 965                    let style = theme
 966                        .toolbar_dropdown_menu
 967                        .item
 968                        .in_state(rpc_trace_selected)
 969                        .style_for(state);
 970                    Flex::row()
 971                        .with_child(
 972                            Label::new(RPC_MESSAGES, style.text.clone())
 973                                .constrained()
 974                                .with_height(theme.toolbar_dropdown_menu.row_height),
 975                        )
 976                        .with_child(
 977                            ui::checkbox_with_label::<Self, _, Self, _>(
 978                                Empty::new(),
 979                                &theme.welcome.checkbox,
 980                                rpc_trace_enabled,
 981                                id.0,
 982                                cx,
 983                                move |this, enabled, cx| {
 984                                    this.toggle_logging_for_server(id, enabled, cx);
 985                                },
 986                            )
 987                            .flex_float(),
 988                        )
 989                        .align_children_center()
 990                        .contained()
 991                        .with_style(style.container)
 992                        .constrained()
 993                        .with_height(theme.toolbar_dropdown_menu.row_height)
 994                })
 995                .with_cursor_style(CursorStyle::PointingHand)
 996                .on_click(MouseButton::Left, move |_, view, cx| {
 997                    view.show_rpc_trace_for_server(id, cx);
 998                }),
 999            )
1000    }
1001}
1002
1003pub enum Event {
1004    NewServerLogEntry {
1005        id: LanguageServerId,
1006        entry: String,
1007        is_rpc: bool,
1008    },
1009}
1010
1011impl Entity for LogStore {
1012    type Event = Event;
1013}
1014
1015impl Entity for LspLogView {
1016    type Event = editor::Event;
1017}
1018
1019impl Entity for LspLogToolbarItemView {
1020    type Event = ();
1021}