Persist LSP logs after log view is closed

This commit is contained in:
Max Brunsfeld 2023-04-21 16:46:58 -07:00
parent 6ef6f03322
commit 6ed7f1281f
3 changed files with 293 additions and 140 deletions

View file

@ -1,4 +1,4 @@
use collections::HashMap; use collections::{hash_map, HashMap};
use editor::Editor; use editor::Editor;
use futures::{channel::mpsc, StreamExt}; use futures::{channel::mpsc, StreamExt};
use gpui::{ use gpui::{
@ -7,15 +7,15 @@ use gpui::{
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, Stack, ParentElement, Stack,
}, },
platform::MouseButton, platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
ViewHandle, WeakModelHandle,
}; };
use language::{Buffer, LanguageServerId, LanguageServerName}; use language::{Buffer, LanguageServerId, LanguageServerName};
use project::{Project, WorktreeId}; use project::{Project, WorktreeId};
use settings::Settings; use settings::Settings;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
use theme::Theme; use theme::{ui, Theme};
use util::ResultExt;
use workspace::{ use workspace::{
item::{Item, ItemHandle}, item::{Item, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemLocation, ToolbarItemView, Workspace,
@ -24,11 +24,27 @@ use workspace::{
const SEND_LINE: &str = "// Send:\n"; const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n"; const RECEIVE_LINE: &str = "// Receive:\n";
struct LogStore {
projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
}
struct LogStoreProject {
servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
_subscription: gpui::Subscription,
}
struct LogStoreLanguageServer {
buffer: ModelHandle<Buffer>,
last_message_kind: Option<MessageKind>,
_subscription: lsp::Subscription,
}
pub struct LspLogView { pub struct LspLogView {
enabled_logs: HashMap<LanguageServerId, LogState>, log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>, current_server_id: Option<LanguageServerId>,
editor: Option<ViewHandle<Editor>>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
io_tx: mpsc::UnboundedSender<(LanguageServerId, bool, String)>,
} }
pub struct LspLogToolbarItemView { pub struct LspLogToolbarItemView {
@ -37,13 +53,6 @@ pub struct LspLogToolbarItemView {
project: ModelHandle<Project>, project: ModelHandle<Project>,
} }
struct LogState {
buffer: ModelHandle<Buffer>,
editor: ViewHandle<Editor>,
last_message_kind: Option<MessageKind>,
_subscription: lsp::Subscription,
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
enum MessageKind { enum MessageKind {
Send, Send,
@ -53,26 +62,37 @@ enum MessageKind {
actions!(log, [OpenLanguageServerLogs]); actions!(log, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) { 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 { impl LogStore {
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self { fn new(cx: &mut ModelContext<Self>) -> Self {
let (io_tx, mut io_rx) = mpsc::unbounded(); let (io_tx, mut io_rx) = mpsc::unbounded();
let this = Self { let this = Self {
enabled_logs: HashMap::default(), projects: HashMap::default(),
current_server_id: None,
io_tx, io_tx,
project,
}; };
cx.spawn_weak(|this, mut cx| async move { 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) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
message.push('\n'); message.push('\n');
this.on_io(language_server_id, is_output, &message, cx); this.on_io(project, server_id, is_output, &message, cx);
}) });
.log_err();
} }
} }
anyhow::Ok(()) anyhow::Ok(())
@ -81,34 +101,90 @@ impl LspLogView {
this this
} }
fn open( pub fn has_enabled_logs_for_language_server(
workspace: &mut Workspace, &self,
_: &OpenLanguageServerLogs, project: &ModelHandle<Project>,
cx: &mut ViewContext<Workspace>, 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<Project>,
server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
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<Project>,
server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) { ) {
let project = workspace.project().read(cx); let project = project.downgrade();
if project.is_remote() { if let Some(store) = self.projects.get_mut(&project) {
return; store.servers.remove(&server_id);
if store.servers.is_empty() {
self.projects.remove(&project);
} }
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>) {
self.enable_logs_for_language_server(server_id, cx);
self.current_server_id = Some(server_id);
cx.notify();
} }
fn on_io( fn on_io(
&mut self, &mut self,
project: WeakModelHandle<Project>,
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
is_received: bool, is_received: bool,
message: &str, message: &str,
cx: &mut ViewContext<Self>, cx: &mut AppContext,
) { ) -> Option<()> {
if let Some(state) = self.enabled_logs.get_mut(&language_server_id) { let state = self
.projects
.get_mut(&project)?
.servers
.get_mut(&language_server_id)?;
state.buffer.update(cx, |buffer, cx| { state.buffer.update(cx, |buffer, cx| {
let kind = if is_received { let kind = if is_received {
MessageKind::Receive MessageKind::Receive
@ -127,50 +203,54 @@ impl LspLogView {
let len = buffer.len(); let len = buffer.len();
buffer.edit([(len..len, message)], None, cx); buffer.edit([(len..len, message)], None, cx);
}); });
Some(())
}
}
impl LspLogView {
fn new(
project: ModelHandle<Project>,
log_set: ModelHandle<LogStore>,
_: &mut ViewContext<Self>,
) -> Self {
Self {
project,
log_store: log_set,
editor: None,
current_server_id: None,
} }
} }
pub fn enable_logs_for_language_server( fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
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, &mut self,
server_id: LanguageServerId, server_id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let Some(server) = self.project.read(cx).language_server_for_id(server_id) { self.log_store.update(cx, |log_store, cx| {
self.enabled_logs.entry(server_id).or_insert_with(|| { if enabled {
let project = self.project.read(cx); log_store.enable_logs_for_language_server(&self.project, server_id, cx);
let io_tx = self.io_tx.clone(); } else {
let language = project.languages().language_for_name("JSON"); log_store.disable_logs_for_language_server(&self.project, server_id, cx);
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();
}),
} }
}); });
} }
}
} }
impl View for LspLogView { impl View for LspLogView {
@ -179,13 +259,12 @@ impl View for LspLogView {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(id) = self.current_server_id { if let Some(editor) = &self.editor {
if let Some(log) = self.enabled_logs.get_mut(&id) { ChildView::new(&editor, cx).into_any()
return ChildView::new(&log.editor, cx).into_any(); } else {
}
}
Empty::new().into_any() Empty::new().into_any()
} }
}
} }
impl Item for LspLogView { impl Item for LspLogView {
@ -195,7 +274,7 @@ impl Item for LspLogView {
style: &theme::Tab, style: &theme::Tab,
_: &AppContext, _: &AppContext,
) -> AnyElement<V> { ) -> AnyElement<V> {
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::<Settings>().theme.clone(); let theme = cx.global::<Settings>().theme.clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
let project = self.project.read(cx); let project = self.project.read(cx);
let mut language_servers = project.language_servers().collect::<Vec<_>>(); let log_view = log_view.read(cx);
language_servers.sort_by_key(|a| a.0); 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::<Vec<_>>();
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| { let current_server = current_server_id.and_then(|current_server_id| {
if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) { if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
Some(language_servers[ix].clone()) Some(language_servers[ix].clone())
@ -252,11 +345,13 @@ impl View for LspLogToolbarItemView {
Overlay::new( Overlay::new(
Flex::column() Flex::column()
.with_children(language_servers.into_iter().filter_map( .with_children(language_servers.into_iter().filter_map(
|(id, name, worktree_id)| { |(id, name, worktree_id, logging_enabled)| {
Self::render_language_server_menu_item( Self::render_language_server_menu_item(
id, id,
name, name,
worktree_id, worktree_id,
logging_enabled,
Some(id) == current_server_id,
&self.project, &self.project,
&theme, &theme,
cx, cx,
@ -266,19 +361,22 @@ impl View for LspLogToolbarItemView {
.contained() .contained()
.with_style(theme.contacts_popover.container) .with_style(theme.contacts_popover.container)
.constrained() .constrained()
.with_width(200.) .with_width(400.)
.with_height(400.), .with_height(400.),
) )
.with_fit_mode(OverlayFitMode::SwitchAnchor) .with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopRight) .with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999) .with_z_index(999)
.aligned() .aligned()
.bottom() .bottom()
.right(), .left(),
) )
} else { } else {
None None
}) })
.aligned()
.left()
.clipped()
.into_any() .into_any()
} }
} }
@ -297,10 +395,29 @@ impl LspLogToolbarItemView {
cx.notify(); cx.notify();
} }
fn activate_log_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) { fn toggle_logging_for_server(
&mut self,
id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
if let Some(log_view) = &self.log_view { if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| { 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<Self>) {
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; self.menu_open = false;
} }
@ -308,7 +425,7 @@ impl LspLogToolbarItemView {
} }
fn render_language_server_menu_header( fn render_language_server_menu_header(
current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId)>, current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
project: &ModelHandle<Project>, project: &ModelHandle<Project>,
theme: &Arc<Theme>, theme: &Arc<Theme>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -317,7 +434,7 @@ impl LspLogToolbarItemView {
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| { MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
let project = project.read(cx); let project = project.read(cx);
let label: Cow<str> = current_server let label: Cow<str> = 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 = project.worktree_for_id(worktree_id, cx)?;
let worktree = &worktree.read(cx); let worktree = &worktree.read(cx);
Some(format!("{} - ({})", server_name.0, worktree.root_name()).into()) Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
@ -325,6 +442,7 @@ impl LspLogToolbarItemView {
.unwrap_or_else(|| "No server selected".into()); .unwrap_or_else(|| "No server selected".into());
Label::new(label, theme.context_menu.item.default.label.clone()) Label::new(label, theme.context_menu.item.default.label.clone())
}) })
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| { .on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx); view.toggle_menu(cx);
}) })
@ -334,6 +452,8 @@ impl LspLogToolbarItemView {
id: LanguageServerId, id: LanguageServerId,
name: LanguageServerName, name: LanguageServerName,
worktree_id: WorktreeId, worktree_id: WorktreeId,
logging_enabled: bool,
is_selected: bool,
project: &ModelHandle<Project>, project: &ModelHandle<Project>,
theme: &Arc<Theme>, theme: &Arc<Theme>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -349,15 +469,34 @@ impl LspLogToolbarItemView {
Some( Some(
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| { MouseEventHandler::<ActivateLog, _>::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::<Self, _, Self, _>(
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| { .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 { impl Entity for LspLogView {
type Event = (); type Event = ();
} }

View file

@ -27,28 +27,40 @@ pub struct CheckboxStyle {
pub hovered_and_checked: ContainerStyle, pub hovered_and_checked: ContainerStyle,
} }
pub fn checkbox<Tag: 'static, V: View>( pub fn checkbox<Tag, V, F>(
label: &'static str, label: &'static str,
style: &CheckboxStyle, style: &CheckboxStyle,
checked: bool, checked: bool,
id: usize,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
change: fn(checked: bool, cx: &mut EventContext<V>) -> (), change: F,
) -> MouseEventHandler<Tag, V> { ) -> MouseEventHandler<Tag, V>
where
Tag: 'static,
V: View,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
let label = Label::new(label, style.label.text.clone()) let label = Label::new(label, style.label.text.clone())
.contained() .contained()
.with_style(style.label.container); .with_style(style.label.container);
checkbox_with_label(label, style, checked, id, cx, change)
checkbox_with_label(label, style, checked, cx, change)
} }
pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>( pub fn checkbox_with_label<Tag, D, V, F>(
label: D, label: D,
style: &CheckboxStyle, style: &CheckboxStyle,
checked: bool, checked: bool,
id: usize,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
change: fn(checked: bool, cx: &mut EventContext<V>) -> (), change: F,
) -> MouseEventHandler<Tag, V> { ) -> MouseEventHandler<Tag, V>
MouseEventHandler::new(0, cx, |state, _| { where
Tag: 'static,
D: Element<V>,
V: View,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
MouseEventHandler::new(id, cx, |state, _| {
let indicator = if checked { let indicator = if checked {
svg(&style.icon) svg(&style.icon)
} else { } else {
@ -75,8 +87,8 @@ pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>(
.with_child(label) .with_child(label)
.align_children_center() .align_children_center()
}) })
.on_click(platform::MouseButton::Left, move |_, _, cx| { .on_click(platform::MouseButton::Left, move |_, view, cx| {
change(!checked, cx) change(view, !checked, cx)
}) })
.with_cursor_style(platform::CursorStyle::PointingHand) .with_cursor_style(platform::CursorStyle::PointingHand)
} }

View file

@ -126,7 +126,7 @@ impl View for WelcomePage {
.with_child( .with_child(
Flex::column() Flex::column()
.with_child( .with_child(
theme::ui::checkbox_with_label::<Metrics, _, Self>( theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
Flex::column() Flex::column()
.with_child( .with_child(
Label::new( Label::new(
@ -146,8 +146,9 @@ impl View for WelcomePage {
), ),
&theme.welcome.checkbox, &theme.welcome.checkbox,
metrics, metrics,
0,
cx, cx,
|checked, cx| { |_, checked, cx| {
SettingsFile::update(cx, move |file| { SettingsFile::update(cx, move |file| {
file.telemetry.set_metrics(checked) file.telemetry.set_metrics(checked)
}) })
@ -157,12 +158,13 @@ impl View for WelcomePage {
.with_style(theme.welcome.checkbox_container), .with_style(theme.welcome.checkbox_container),
) )
.with_child( .with_child(
theme::ui::checkbox::<Diagnostics, Self>( theme::ui::checkbox::<Diagnostics, Self, _>(
"Send crash reports", "Send crash reports",
&theme.welcome.checkbox, &theme.welcome.checkbox,
diagnostics, diagnostics,
0,
cx, cx,
|checked, cx| { |_, checked, cx| {
SettingsFile::update(cx, move |file| { SettingsFile::update(cx, move |file| {
file.telemetry.set_diagnostics(checked) file.telemetry.set_diagnostics(checked)
}) })