@@ -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<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 {
- enabled_logs: HashMap<LanguageServerId, LogState>,
+ log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
+ editor: Option<ViewHandle<Editor>>,
project: ModelHandle<Project>,
- io_tx: mpsc::UnboundedSender<(LanguageServerId, bool, String)>,
}
pub struct LspLogToolbarItemView {
@@ -37,13 +53,6 @@ pub struct LspLogToolbarItemView {
project: ModelHandle<Project>,
}
-struct LogState {
- buffer: ModelHandle<Buffer>,
- editor: ViewHandle<Editor>,
- last_message_kind: Option<MessageKind>,
- _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<Project>, cx: &mut ViewContext<Self>) -> Self {
+impl LogStore {
+ fn new(cx: &mut ModelContext<Self>) -> 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<Workspace>,
- ) {
- let project = workspace.project().read(cx);
- if project.is_remote() {
- return;
- }
+ pub fn has_enabled_logs_for_language_server(
+ &self,
+ project: &ModelHandle<Project>,
+ 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 log_view = cx.add_view(|cx| Self::new(workspace.project().clone(), cx));
- workspace.add_item(Box::new(log_view), cx);
+ 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())
}
- 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();
+ pub fn disable_logs_for_language_server(
+ &mut self,
+ project: &ModelHandle<Project>,
+ server_id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Project>,
language_server_id: LanguageServerId,
is_received: bool,
message: &str,
- cx: &mut ViewContext<Self>,
- ) {
- 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);
- }
+ 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();
- buffer.edit([(len..len, message)], None, cx);
- });
+ 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<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,
server_id: LanguageServerId,
+ enabled: bool,
cx: &mut ViewContext<Self>,
) {
- 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<Self>) -> AnyElement<Self> {
- 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<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 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::<Vec<_>>();
- 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 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.read(cx).current_server_id;
+ 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<Self>) {
+ fn toggle_logging_for_server(
+ &mut self,
+ id: LanguageServerId,
+ enabled: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
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<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;
}
@@ -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<Project>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
@@ -317,7 +434,7 @@ impl LspLogToolbarItemView {
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
let project = project.read(cx);
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 = &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<Project>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
@@ -349,15 +469,34 @@ impl LspLogToolbarItemView {
Some(
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| {
- 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 = ();
}
@@ -27,28 +27,40 @@ pub struct CheckboxStyle {
pub hovered_and_checked: ContainerStyle,
}
-pub fn checkbox<Tag: 'static, V: View>(
+pub fn checkbox<Tag, V, F>(
label: &'static str,
style: &CheckboxStyle,
checked: bool,
+ id: usize,
cx: &mut ViewContext<V>,
- change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
-) -> MouseEventHandler<Tag, V> {
+ change: F,
+) -> 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())
.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<Tag: 'static, D: Element<V>, V: View>(
+pub fn checkbox_with_label<Tag, D, V, F>(
label: D,
style: &CheckboxStyle,
checked: bool,
+ id: usize,
cx: &mut ViewContext<V>,
- change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
-) -> MouseEventHandler<Tag, V> {
- MouseEventHandler::new(0, cx, |state, _| {
+ change: F,
+) -> MouseEventHandler<Tag, V>
+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 {
svg(&style.icon)
} else {
@@ -75,8 +87,8 @@ pub fn checkbox_with_label<Tag: 'static, D: Element<V>, 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)
}