lsp_log.rs

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