diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs index 14329dbbd3..6030070006 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -1,4 +1,4 @@ -use collections::HashMap; +use collections::{hash_map, HashMap}; use editor::Editor; use futures::{channel::mpsc, StreamExt}; use gpui::{ @@ -7,15 +7,15 @@ use gpui::{ AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, ParentElement, Stack, }, - platform::MouseButton, - AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, + platform::{CursorStyle, MouseButton}, + AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext, + ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; use project::{Project, WorktreeId}; use settings::Settings; use std::{borrow::Cow, sync::Arc}; -use theme::Theme; -use util::ResultExt; +use theme::{ui, Theme}; use workspace::{ item::{Item, ItemHandle}, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -24,11 +24,27 @@ use workspace::{ const SEND_LINE: &str = "// Send:\n"; const RECEIVE_LINE: &str = "// Receive:\n"; +struct LogStore { + projects: HashMap, LogStoreProject>, + io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, +} + +struct LogStoreProject { + servers: HashMap, + _subscription: gpui::Subscription, +} + +struct LogStoreLanguageServer { + buffer: ModelHandle, + last_message_kind: Option, + _subscription: lsp::Subscription, +} + pub struct LspLogView { - enabled_logs: HashMap, + log_store: ModelHandle, current_server_id: Option, + editor: Option>, project: ModelHandle, - io_tx: mpsc::UnboundedSender<(LanguageServerId, bool, String)>, } pub struct LspLogToolbarItemView { @@ -37,13 +53,6 @@ pub struct LspLogToolbarItemView { project: ModelHandle, } -struct LogState { - buffer: ModelHandle, - editor: ViewHandle, - last_message_kind: Option, - _subscription: lsp::Subscription, -} - #[derive(Copy, Clone, PartialEq, Eq)] enum MessageKind { Send, @@ -53,26 +62,37 @@ enum MessageKind { actions!(log, [OpenLanguageServerLogs]); pub fn init(cx: &mut AppContext) { - cx.add_action(LspLogView::open); + let log_set = cx.add_model(|cx| LogStore::new(cx)); + + cx.add_action( + move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| { + let project = workspace.project().read(cx); + if project.is_local() { + workspace.add_item( + Box::new(cx.add_view(|cx| { + LspLogView::new(workspace.project().clone(), log_set.clone(), cx) + })), + cx, + ); + } + }, + ); } -impl LspLogView { - pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { +impl LogStore { + fn new(cx: &mut ModelContext) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let this = Self { - enabled_logs: HashMap::default(), - current_server_id: None, + projects: HashMap::default(), io_tx, - project, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((language_server_id, is_output, mut message)) = io_rx.next().await { + while let Some((project, server_id, is_output, mut message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { message.push('\n'); - this.on_io(language_server_id, is_output, &message, cx); - }) - .log_err(); + this.on_io(project, server_id, is_output, &message, cx); + }); } } anyhow::Ok(()) @@ -81,95 +101,155 @@ impl LspLogView { this } - fn open( - workspace: &mut Workspace, - _: &OpenLanguageServerLogs, - cx: &mut ViewContext, - ) { - let project = workspace.project().read(cx); - if project.is_remote() { - return; - } - - let log_view = cx.add_view(|cx| Self::new(workspace.project().clone(), cx)); - workspace.add_item(Box::new(log_view), cx); - } - - fn activate_log(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { - self.enable_logs_for_language_server(server_id, cx); - self.current_server_id = Some(server_id); - cx.notify(); - } - - fn on_io( - &mut self, - language_server_id: LanguageServerId, - is_received: bool, - message: &str, - cx: &mut ViewContext, - ) { - if let Some(state) = self.enabled_logs.get_mut(&language_server_id) { - state.buffer.update(cx, |buffer, cx| { - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send - }; - if state.last_message_kind != Some(kind) { - let len = buffer.len(); - let line = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - buffer.edit([(len..len, line)], None, cx); - state.last_message_kind = Some(kind); - } - let len = buffer.len(); - buffer.edit([(len..len, message)], None, cx); - }); - } + pub fn has_enabled_logs_for_language_server( + &self, + project: &ModelHandle, + server_id: LanguageServerId, + ) -> bool { + self.projects + .get(&project.downgrade()) + .map_or(false, |store| store.servers.contains_key(&server_id)) } pub fn enable_logs_for_language_server( &mut self, + project: &ModelHandle, server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option> { + let server = project.read(cx).language_server_for_id(server_id)?; + let weak_project = project.downgrade(); + let project_logs = match self.projects.entry(weak_project) { + hash_map::Entry::Occupied(entry) => entry.into_mut(), + hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject { + servers: HashMap::default(), + _subscription: cx.observe_release(&project, move |this, _, cx| { + this.projects.remove(&weak_project); + }), + }), + }; + let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| { + let io_tx = self.io_tx.clone(); + let language = project.read(cx).languages().language_for_name("JSON"); + let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + cx.spawn_weak({ + let buffer = buffer.clone(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }); + } + }) + .detach(); + + let project = project.downgrade(); + LogStoreLanguageServer { + buffer, + last_message_kind: None, + _subscription: server.on_io(move |is_received, json| { + io_tx + .unbounded_send((project, server_id, is_received, json.to_string())) + .ok(); + }), + } + }); + Some(server_log_state.buffer.clone()) + } + + pub fn disable_logs_for_language_server( + &mut self, + project: &ModelHandle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) { + let project = project.downgrade(); + if let Some(store) = self.projects.get_mut(&project) { + store.servers.remove(&server_id); + if store.servers.is_empty() { + self.projects.remove(&project); + } + } + } + + fn on_io( + &mut self, + project: WeakModelHandle, + language_server_id: LanguageServerId, + is_received: bool, + message: &str, + cx: &mut AppContext, + ) -> Option<()> { + let state = self + .projects + .get_mut(&project)? + .servers + .get_mut(&language_server_id)?; + state.buffer.update(cx, |buffer, cx| { + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + if state.last_message_kind != Some(kind) { + let len = buffer.len(); + let line = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + buffer.edit([(len..len, line)], None, cx); + state.last_message_kind = Some(kind); + } + let len = buffer.len(); + buffer.edit([(len..len, message)], None, cx); + }); + Some(()) + } +} + +impl LspLogView { + fn new( + project: ModelHandle, + log_set: ModelHandle, + _: &mut ViewContext, + ) -> Self { + Self { + project, + log_store: log_set, + editor: None, + current_server_id: None, + } + } + + fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { + let buffer = self.log_store.update(cx, |log_set, cx| { + log_set.enable_logs_for_language_server(&self.project, server_id, cx) + }); + if let Some(buffer) = buffer { + self.current_server_id = Some(server_id); + self.editor = Some(cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx); + editor.set_read_only(true); + editor.move_to_end(&Default::default(), cx); + editor + })); + cx.notify(); + } + } + + fn toggle_logging_for_server( + &mut self, + server_id: LanguageServerId, + enabled: bool, cx: &mut ViewContext, ) { - if let Some(server) = self.project.read(cx).language_server_for_id(server_id) { - self.enabled_logs.entry(server_id).or_insert_with(|| { - let project = self.project.read(cx); - let io_tx = self.io_tx.clone(); - let language = project.languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); - cx.spawn({ - let buffer = buffer.clone(); - |_, mut cx| async move { - let language = language.await.ok(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(language, cx); - }); - } - }) - .detach(); - let editor = cx.add_view(|cx| { - let mut editor = - Editor::for_buffer(buffer.clone(), Some(self.project.clone()), cx); - editor.set_read_only(true); - editor - }); - - LogState { - buffer, - editor, - last_message_kind: None, - _subscription: server.on_io(move |is_received, json| { - io_tx - .unbounded_send((server_id, is_received, json.to_string())) - .ok(); - }), - } - }); - } + self.log_store.update(cx, |log_store, cx| { + if enabled { + log_store.enable_logs_for_language_server(&self.project, server_id, cx); + } else { + log_store.disable_logs_for_language_server(&self.project, server_id, cx); + } + }); } } @@ -179,12 +259,11 @@ impl View for LspLogView { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(id) = self.current_server_id { - if let Some(log) = self.enabled_logs.get_mut(&id) { - return ChildView::new(&log.editor, cx).into_any(); - } + if let Some(editor) = &self.editor { + ChildView::new(&editor, cx).into_any() + } else { + Empty::new().into_any() } - Empty::new().into_any() } } @@ -195,7 +274,7 @@ impl Item for LspLogView { style: &theme::Tab, _: &AppContext, ) -> AnyElement { - Label::new("Logs", style.label.clone()).into_any() + Label::new("LSP Logs", style.label.clone()).into_any() } } @@ -228,10 +307,24 @@ impl View for LspLogToolbarItemView { let theme = cx.global::().theme.clone(); let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; let project = self.project.read(cx); - let mut language_servers = project.language_servers().collect::>(); - language_servers.sort_by_key(|a| a.0); + let log_view = log_view.read(cx); + let log_store = log_view.log_store.read(cx); - let current_server_id = log_view.read(cx).current_server_id; + let mut language_servers = project + .language_servers() + .map(|(id, name, worktree)| { + ( + id, + name, + worktree, + log_store.has_enabled_logs_for_language_server(&self.project, id), + ) + }) + .collect::>(); + language_servers.sort_by_key(|a| (a.0, a.2)); + language_servers.dedup_by_key(|a| a.0); + + let current_server_id = log_view.current_server_id; let current_server = current_server_id.and_then(|current_server_id| { if let Ok(ix) = language_servers.binary_search_by_key(¤t_server_id, |e| e.0) { Some(language_servers[ix].clone()) @@ -252,11 +345,13 @@ impl View for LspLogToolbarItemView { Overlay::new( Flex::column() .with_children(language_servers.into_iter().filter_map( - |(id, name, worktree_id)| { + |(id, name, worktree_id, logging_enabled)| { Self::render_language_server_menu_item( id, name, worktree_id, + logging_enabled, + Some(id) == current_server_id, &self.project, &theme, cx, @@ -266,19 +361,22 @@ impl View for LspLogToolbarItemView { .contained() .with_style(theme.contacts_popover.container) .constrained() - .with_width(200.) + .with_width(400.) .with_height(400.), ) .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopRight) + .with_anchor_corner(AnchorCorner::TopLeft) .with_z_index(999) .aligned() .bottom() - .right(), + .left(), ) } else { None }) + .aligned() + .left() + .clipped() .into_any() } } @@ -297,10 +395,29 @@ impl LspLogToolbarItemView { cx.notify(); } - fn activate_log_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { + fn toggle_logging_for_server( + &mut self, + id: LanguageServerId, + enabled: bool, + cx: &mut ViewContext, + ) { if let Some(log_view) = &self.log_view { log_view.update(cx, |log_view, cx| { - log_view.activate_log(id, cx); + log_view.toggle_logging_for_server(id, enabled, cx); + if !enabled && Some(id) == log_view.current_server_id { + log_view.current_server_id = None; + cx.notify(); + } + }); + self.menu_open = false; + } + cx.notify(); + } + + fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |log_view, cx| { + log_view.show_logs_for_server(id, cx); }); self.menu_open = false; } @@ -308,7 +425,7 @@ impl LspLogToolbarItemView { } fn render_language_server_menu_header( - current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId)>, + current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>, project: &ModelHandle, theme: &Arc, cx: &mut ViewContext, @@ -317,7 +434,7 @@ impl LspLogToolbarItemView { MouseEventHandler::::new(0, cx, move |state, cx| { let project = project.read(cx); let label: Cow = current_server - .and_then(|(_, server_name, worktree_id)| { + .and_then(|(_, server_name, worktree_id, _)| { let worktree = project.worktree_for_id(worktree_id, cx)?; let worktree = &worktree.read(cx); Some(format!("{} - ({})", server_name.0, worktree.root_name()).into()) @@ -325,6 +442,7 @@ impl LspLogToolbarItemView { .unwrap_or_else(|| "No server selected".into()); Label::new(label, theme.context_menu.item.default.label.clone()) }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { view.toggle_menu(cx); }) @@ -334,6 +452,8 @@ impl LspLogToolbarItemView { id: LanguageServerId, name: LanguageServerName, worktree_id: WorktreeId, + logging_enabled: bool, + is_selected: bool, project: &ModelHandle, theme: &Arc, cx: &mut ViewContext, @@ -349,15 +469,34 @@ impl LspLogToolbarItemView { Some( MouseEventHandler::::new(id.0, cx, move |state, cx| { - Label::new(label, theme.context_menu.item.default.label.clone()) + let item_style = theme.context_menu.item.style_for(state, is_selected); + Flex::row() + .with_child(ui::checkbox_with_label::( + Empty::new(), + &theme.welcome.checkbox, + logging_enabled, + id.0, + cx, + move |this, enabled, cx| { + this.toggle_logging_for_server(id, enabled, cx); + }, + )) + .with_child(Label::new(label, item_style.label.clone())) + .contained() + .with_style(item_style.container) }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { - view.activate_log_for_server(id, cx); + view.show_logs_for_server(id, cx); }), ) } } +impl Entity for LogStore { + type Event = (); +} + impl Entity for LspLogView { type Event = (); } diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 4e705b12b8..0f58ee08ad 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -27,28 +27,40 @@ pub struct CheckboxStyle { pub hovered_and_checked: ContainerStyle, } -pub fn checkbox( +pub fn checkbox( label: &'static str, style: &CheckboxStyle, checked: bool, + id: usize, cx: &mut ViewContext, - change: fn(checked: bool, cx: &mut EventContext) -> (), -) -> MouseEventHandler { + change: F, +) -> MouseEventHandler +where + Tag: 'static, + V: View, + F: 'static + Fn(&mut V, bool, &mut EventContext), +{ let label = Label::new(label, style.label.text.clone()) .contained() .with_style(style.label.container); - - checkbox_with_label(label, style, checked, cx, change) + checkbox_with_label(label, style, checked, id, cx, change) } -pub fn checkbox_with_label, V: View>( +pub fn checkbox_with_label( label: D, style: &CheckboxStyle, checked: bool, + id: usize, cx: &mut ViewContext, - change: fn(checked: bool, cx: &mut EventContext) -> (), -) -> MouseEventHandler { - MouseEventHandler::new(0, cx, |state, _| { + change: F, +) -> MouseEventHandler +where + Tag: 'static, + D: Element, + V: View, + F: 'static + Fn(&mut V, bool, &mut EventContext), +{ + MouseEventHandler::new(id, cx, |state, _| { let indicator = if checked { svg(&style.icon) } else { @@ -75,8 +87,8 @@ pub fn checkbox_with_label, V: View>( .with_child(label) .align_children_center() }) - .on_click(platform::MouseButton::Left, move |_, _, cx| { - change(!checked, cx) + .on_click(platform::MouseButton::Left, move |_, view, cx| { + change(view, !checked, cx) }) .with_cursor_style(platform::CursorStyle::PointingHand) } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index d75bbea56e..629e6f3989 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -126,7 +126,7 @@ impl View for WelcomePage { .with_child( Flex::column() .with_child( - theme::ui::checkbox_with_label::( + theme::ui::checkbox_with_label::( Flex::column() .with_child( Label::new( @@ -146,8 +146,9 @@ impl View for WelcomePage { ), &theme.welcome.checkbox, metrics, + 0, cx, - |checked, cx| { + |_, checked, cx| { SettingsFile::update(cx, move |file| { file.telemetry.set_metrics(checked) }) @@ -157,12 +158,13 @@ impl View for WelcomePage { .with_style(theme.welcome.checkbox_container), ) .with_child( - theme::ui::checkbox::( + theme::ui::checkbox::( "Send crash reports", &theme.welcome.checkbox, diagnostics, + 0, cx, - |checked, cx| { + |_, checked, cx| { SettingsFile::update(cx, move |file| { file.telemetry.set_diagnostics(checked) })