Cargo.lock 🔗
@@ -3764,8 +3764,10 @@ name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
+ "client",
"collections",
"editor",
+ "env_logger 0.9.3",
"futures 0.3.28",
"gpui",
"language",
Max Brunsfeld created
In debugging what's going on with the Elixir language server, there was
some interesting content in the server's logs (sent to the app via the
`window/logMessage` LSP endpoint). I decided to invest in making
language server issues easier to debug by exposing these `logMessage`
contents in the app.
Also, improve the UI of the view slightly:
* Select one of the servers by default (instead of "no server selected")
* Make it clearer that the menu is clickable
Cargo.lock | 2
crates/lsp/src/lsp.rs | 9
crates/lsp_log/Cargo.toml | 2
crates/lsp_log/src/lsp_log.rs | 489 +++++++++++++++++++++---------
crates/lsp_log/src/lsp_log_tests.rs | 97 ++++++
crates/project/src/project.rs | 60 ++-
crates/project/src/project_tests.rs | 9
crates/theme/src/theme.rs | 11
crates/workspace/src/workspace.rs | 2
crates/zed/src/zed.rs | 5
styles/src/styleTree/app.ts | 2
styles/src/styleTree/lspLogMenu.ts | 42 ++
12 files changed, 555 insertions(+), 175 deletions(-)
@@ -3764,8 +3764,10 @@ name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
+ "client",
"collections",
"editor",
+ "env_logger 0.9.3",
"futures 0.3.28",
"gpui",
"language",
@@ -748,6 +748,15 @@ impl fmt::Display for LanguageServerId {
}
}
+impl fmt::Debug for LanguageServer {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("LanguageServer")
+ .field("id", &self.server_id.0)
+ .field("name", &self.name)
+ .finish_non_exhaustive()
+ }
+}
+
impl Drop for Subscription {
fn drop(&mut self) {
match self {
@@ -24,7 +24,9 @@ serde.workspace = true
anyhow.workspace = true
[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
+env_logger.workspace = true
unindent.workspace = true
@@ -1,4 +1,7 @@
-use collections::{hash_map, HashMap};
+#[cfg(test)]
+mod lsp_log_tests;
+
+use collections::HashMap;
use editor::Editor;
use futures::{channel::mpsc, StreamExt};
use gpui::{
@@ -12,28 +15,33 @@ use gpui::{
ViewHandle, WeakModelHandle,
};
use language::{Buffer, LanguageServerId, LanguageServerName};
-use project::{Project, WorktreeId};
+use project::{Project, Worktree};
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use workspace::{
item::{Item, ItemHandle},
- ToolbarItemLocation, ToolbarItemView, Workspace,
+ ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
};
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
struct LogStore {
- projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
+ projects: HashMap<WeakModelHandle<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
}
-struct LogStoreProject {
- servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
- _subscription: gpui::Subscription,
+struct ProjectState {
+ servers: HashMap<LanguageServerId, LanguageServerState>,
+ _subscriptions: [gpui::Subscription; 2],
+}
+
+struct LanguageServerState {
+ log_buffer: ModelHandle<Buffer>,
+ rpc_state: Option<LanguageServerRpcState>,
}
-struct LogStoreLanguageServer {
+struct LanguageServerRpcState {
buffer: ModelHandle<Buffer>,
last_message_kind: Option<MessageKind>,
_subscription: lsp::Subscription,
@@ -42,6 +50,7 @@ struct LogStoreLanguageServer {
pub struct LspLogView {
log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
+ is_showing_rpc_trace: bool,
editor: Option<ViewHandle<Editor>>,
project: ModelHandle<Project>,
}
@@ -49,7 +58,6 @@ pub struct LspLogView {
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
menu_open: bool,
- project: ModelHandle<Project>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -58,10 +66,36 @@ enum MessageKind {
Receive,
}
+#[derive(Clone, Debug, PartialEq)]
+struct LogMenuItem {
+ server_id: LanguageServerId,
+ server_name: LanguageServerName,
+ worktree: ModelHandle<Worktree>,
+ rpc_trace_enabled: bool,
+ rpc_trace_selected: bool,
+ logs_selected: bool,
+}
+
actions!(log, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
- let log_set = cx.add_model(|cx| LogStore::new(cx));
+ let log_store = cx.add_model(|cx| LogStore::new(cx));
+
+ cx.subscribe_global::<WorkspaceCreated, _>({
+ let log_store = log_store.clone();
+ move |event, cx| {
+ let workspace = &event.0;
+ if let Some(workspace) = workspace.upgrade(cx) {
+ let project = workspace.read(cx).project().clone();
+ if project.read(cx).is_local() {
+ log_store.update(cx, |store, cx| {
+ store.add_project(&project, cx);
+ });
+ }
+ }
+ }
+ })
+ .detach();
cx.add_action(
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
@@ -69,7 +103,7 @@ pub fn init(cx: &mut AppContext) {
if project.is_local() {
workspace.add_item(
Box::new(cx.add_view(|cx| {
- LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
+ LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
})),
cx,
);
@@ -100,34 +134,113 @@ impl LogStore {
this
}
- pub fn has_enabled_logs_for_language_server(
+ pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
+ use project::Event::*;
+
+ let weak_project = project.downgrade();
+ self.projects.insert(
+ weak_project,
+ ProjectState {
+ servers: HashMap::default(),
+ _subscriptions: [
+ cx.observe_release(&project, move |this, _, _| {
+ this.projects.remove(&weak_project);
+ }),
+ cx.subscribe(project, |this, project, event, cx| match event {
+ LanguageServerAdded(id) => {
+ this.add_language_server(&project, *id, cx);
+ }
+ LanguageServerRemoved(id) => {
+ this.remove_language_server(&project, *id, cx);
+ }
+ LanguageServerLog(id, message) => {
+ this.add_language_server_log(&project, *id, message, cx);
+ }
+ _ => {}
+ }),
+ ],
+ },
+ );
+ }
+
+ fn add_language_server(
+ &mut self,
+ project: &ModelHandle<Project>,
+ id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<ModelHandle<Buffer>> {
+ let project_state = self.projects.get_mut(&project.downgrade())?;
+ Some(
+ project_state
+ .servers
+ .entry(id)
+ .or_insert_with(|| {
+ cx.notify();
+ LanguageServerState {
+ rpc_state: None,
+ log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
+ }
+ })
+ .log_buffer
+ .clone(),
+ )
+ }
+
+ fn add_language_server_log(
+ &mut self,
+ project: &ModelHandle<Project>,
+ id: LanguageServerId,
+ message: &str,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<()> {
+ let buffer = self.add_language_server(&project, id, cx)?;
+ buffer.update(cx, |buffer, cx| {
+ let len = buffer.len();
+ let has_newline = message.ends_with("\n");
+ buffer.edit([(len..len, message)], None, cx);
+ if !has_newline {
+ let len = buffer.len();
+ buffer.edit([(len..len, "\n")], None, cx);
+ }
+ });
+ cx.notify();
+ Some(())
+ }
+
+ fn remove_language_server(
+ &mut self,
+ project: &ModelHandle<Project>,
+ id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<()> {
+ let project_state = self.projects.get_mut(&project.downgrade())?;
+ project_state.servers.remove(&id);
+ cx.notify();
+ Some(())
+ }
+
+ pub fn log_buffer_for_server(
&self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
- ) -> bool {
- self.projects
- .get(&project.downgrade())
- .map_or(false, |store| store.servers.contains_key(&server_id))
+ ) -> Option<ModelHandle<Buffer>> {
+ let weak_project = project.downgrade();
+ let project_state = self.projects.get(&weak_project)?;
+ let server_state = project_state.servers.get(&server_id)?;
+ Some(server_state.log_buffer.clone())
}
- pub fn enable_logs_for_language_server(
+ pub fn enable_rpc_trace_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, _, _| {
- this.projects.remove(&weak_project);
- }),
- }),
- };
- let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
+ let project_state = self.projects.get_mut(&weak_project)?;
+ let server_state = project_state.servers.get_mut(&server_id)?;
+ let server = project.read(cx).language_server_for_id(server_id)?;
+ let rpc_state = server_state.rpc_state.get_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));
@@ -142,33 +255,30 @@ impl LogStore {
})
.detach();
- let project = project.downgrade();
- LogStoreLanguageServer {
+ LanguageServerRpcState {
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()))
+ .unbounded_send((weak_project, server_id, is_received, json.to_string()))
.ok();
}),
}
});
- Some(server_log_state.buffer.clone())
+ Some(rpc_state.buffer.clone())
}
- pub fn disable_logs_for_language_server(
+ pub fn disable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
_: &mut ModelContext<Self>,
- ) {
+ ) -> Option<()> {
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);
- }
- }
+ let project_state = self.projects.get_mut(&project)?;
+ let server_state = project_state.servers.get_mut(&server_id)?;
+ server_state.rpc_state.take();
+ Some(())
}
fn on_io(
@@ -183,7 +293,9 @@ impl LogStore {
.projects
.get_mut(&project)?
.servers
- .get_mut(&language_server_id)?;
+ .get_mut(&language_server_id)?
+ .rpc_state
+ .as_mut()?;
state.buffer.update(cx, |buffer, cx| {
let kind = if is_received {
MessageKind::Receive
@@ -209,23 +321,83 @@ impl LogStore {
impl LspLogView {
fn new(
project: ModelHandle<Project>,
- log_set: ModelHandle<LogStore>,
- _: &mut ViewContext<Self>,
+ log_store: ModelHandle<LogStore>,
+ cx: &mut ViewContext<Self>,
) -> Self {
- Self {
+ let server_id = log_store
+ .read(cx)
+ .projects
+ .get(&project.downgrade())
+ .and_then(|project| project.servers.keys().copied().next());
+ let mut this = Self {
project,
- log_store: log_set,
+ log_store,
editor: None,
current_server_id: None,
+ is_showing_rpc_trace: false,
+ };
+ if let Some(server_id) = server_id {
+ this.show_logs_for_server(server_id, cx);
}
+ this
+ }
+
+ fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
+ let log_store = self.log_store.read(cx);
+ let state = log_store.projects.get(&self.project.downgrade())?;
+ let mut rows = self
+ .project
+ .read(cx)
+ .language_servers()
+ .filter_map(|(server_id, language_server_name, worktree_id)| {
+ let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
+ let state = state.servers.get(&server_id)?;
+ Some(LogMenuItem {
+ server_id,
+ server_name: language_server_name,
+ worktree,
+ rpc_trace_enabled: state.rpc_state.is_some(),
+ rpc_trace_selected: self.is_showing_rpc_trace
+ && self.current_server_id == Some(server_id),
+ logs_selected: !self.is_showing_rpc_trace
+ && self.current_server_id == Some(server_id),
+ })
+ })
+ .collect::<Vec<_>>();
+ rows.sort_by_key(|row| row.server_id);
+ rows.dedup_by_key(|row| row.server_id);
+ Some(rows)
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
+ let buffer = self
+ .log_store
+ .read(cx)
+ .log_buffer_for_server(&self.project, server_id);
+ if let Some(buffer) = buffer {
+ self.current_server_id = Some(server_id);
+ self.is_showing_rpc_trace = false;
+ 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 show_rpc_trace_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)
+ log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
});
if let Some(buffer) = buffer {
self.current_server_id = Some(server_id);
+ self.is_showing_rpc_trace = true;
self.editor = Some(cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
editor.set_read_only(true);
@@ -236,7 +408,7 @@ impl LspLogView {
}
}
- fn toggle_logging_for_server(
+ fn toggle_rpc_trace_for_server(
&mut self,
server_id: LanguageServerId,
enabled: bool,
@@ -244,11 +416,15 @@ impl LspLogView {
) {
self.log_store.update(cx, |log_store, cx| {
if enabled {
- log_store.enable_logs_for_language_server(&self.project, server_id, cx);
+ log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
} else {
- log_store.disable_logs_for_language_server(&self.project, server_id, cx);
+ log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
}
});
+ if !enabled && Some(server_id) == self.current_server_id {
+ self.show_logs_for_server(server_id, cx);
+ cx.notify();
+ }
}
}
@@ -305,28 +481,18 @@ impl View for LspLogToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
- let project = self.project.read(cx);
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 menu_rows = self
+ .log_view
+ .as_ref()
+ .and_then(|view| view.read(cx).menu_items(cx))
+ .unwrap_or_default();
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())
+ if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
+ Some(menu_rows[ix].clone())
} else {
None
}
@@ -337,7 +503,6 @@ impl View for LspLogToolbarItemView {
Stack::new()
.with_child(Self::render_language_server_menu_header(
current_server,
- &self.project,
&theme,
cx,
))
@@ -346,22 +511,20 @@ impl View for LspLogToolbarItemView {
Overlay::new(
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
Flex::column()
- .with_children(language_servers.into_iter().filter_map(
- |(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,
- )
- },
- ))
+ .with_children(menu_rows.into_iter().map(|row| {
+ Self::render_language_server_menu_item(
+ row.server_id,
+ row.server_name,
+ row.worktree,
+ row.rpc_trace_enabled,
+ row.logs_selected,
+ row.rpc_trace_selected,
+ &theme,
+ cx,
+ )
+ }))
.contained()
- .with_style(theme.context_menu.container)
+ .with_style(theme.lsp_log_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
@@ -388,12 +551,14 @@ impl View for LspLogToolbarItemView {
}
}
+const RPC_MESSAGES: &str = "RPC Messages";
+const SERVER_LOGS: &str = "Server Logs";
+
impl LspLogToolbarItemView {
- pub fn new(project: ModelHandle<Project>) -> Self {
+ pub fn new() -> Self {
Self {
menu_open: false,
log_view: None,
- project,
}
}
@@ -410,10 +575,9 @@ impl LspLogToolbarItemView {
) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| {
- log_view.toggle_logging_for_server(id, enabled, cx);
+ log_view.toggle_rpc_trace_for_server(id, enabled, cx);
if !enabled && Some(id) == log_view.current_server_id {
- log_view.current_server_id = None;
- log_view.editor = None;
+ log_view.show_logs_for_server(id, cx);
cx.notify();
}
});
@@ -423,39 +587,49 @@ impl LspLogToolbarItemView {
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);
- });
+ log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
+ self.menu_open = false;
+ cx.notify();
+ }
+ }
+
+ fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
+ if let Some(log_view) = &self.log_view {
+ log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
self.menu_open = false;
+ cx.notify();
}
- cx.notify();
}
fn render_language_server_menu_header(
- current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
- project: &ModelHandle<Project>,
+ current_server: Option<LogMenuItem>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
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, _)| {
- let worktree = project.worktree_for_id(worktree_id, cx)?;
- let worktree = &worktree.read(cx);
- Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
+ .and_then(|row| {
+ let worktree = row.worktree.read(cx);
+ Some(
+ format!(
+ "{} ({}) - {}",
+ row.server_name.0,
+ worktree.root_name(),
+ if row.rpc_trace_selected {
+ RPC_MESSAGES
+ } else {
+ SERVER_LOGS
+ },
+ )
+ .into(),
+ )
})
.unwrap_or_else(|| "No server selected".into());
- Label::new(
- label,
- theme
- .context_menu
- .item
- .style_for(state, false)
- .label
- .clone(),
- )
+ let style = theme.lsp_log_menu.header.style_for(state, false);
+ Label::new(label, style.text.clone())
+ .contained()
+ .with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
@@ -466,46 +640,75 @@ impl LspLogToolbarItemView {
fn render_language_server_menu_item(
id: LanguageServerId,
name: LanguageServerName,
- worktree_id: WorktreeId,
- logging_enabled: bool,
- is_selected: bool,
- project: &ModelHandle<Project>,
+ worktree: ModelHandle<Worktree>,
+ rpc_trace_enabled: bool,
+ logs_selected: bool,
+ rpc_trace_selected: bool,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
- ) -> Option<impl Element<Self>> {
+ ) -> impl Element<Self> {
enum ActivateLog {}
- let project = project.read(cx);
- let worktree = project.worktree_for_id(worktree_id, cx)?;
- let worktree = &worktree.read(cx);
- if !worktree.is_visible() {
- return None;
- }
- let label = format!("{} - ({})", name.0, worktree.root_name());
-
- Some(
- MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
- 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()).aligned().left())
- .align_children_center()
- .contained()
- .with_style(item_style.container)
+ enum ActivateRpcTrace {}
+
+ Flex::column()
+ .with_child({
+ let style = &theme.lsp_log_menu.server;
+ Label::new(
+ format!("{} ({})", name.0, worktree.read(cx).root_name()),
+ style.text.clone(),
+ )
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.lsp_log_menu.row_height)
})
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, view, cx| {
- view.show_logs_for_server(id, cx);
- }),
- )
+ .with_child(
+ MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
+ let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
+ Label::new(SERVER_LOGS, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.lsp_log_menu.row_height)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, view, cx| {
+ view.show_logs_for_server(id, cx);
+ }),
+ )
+ .with_child(
+ MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
+ let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected);
+ Flex::row()
+ .with_child(
+ Label::new(RPC_MESSAGES, style.text.clone())
+ .constrained()
+ .with_height(theme.lsp_log_menu.row_height),
+ )
+ .with_child(
+ ui::checkbox_with_label::<Self, _, Self, _>(
+ Empty::new(),
+ &theme.welcome.checkbox,
+ rpc_trace_enabled,
+ id.0,
+ cx,
+ move |this, enabled, cx| {
+ this.toggle_logging_for_server(id, enabled, cx);
+ },
+ )
+ .flex_float(),
+ )
+ .align_children_center()
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.lsp_log_menu.row_height)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, view, cx| {
+ view.show_rpc_trace_for_server(id, cx);
+ }),
+ )
}
}
@@ -0,0 +1,97 @@
+use super::*;
+use gpui::{serde_json::json, TestAppContext};
+use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig};
+use project::FakeFs;
+use settings::SettingsStore;
+
+#[gpui::test]
+async fn test_lsp_logs(cx: &mut TestAppContext) {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+
+ init_test(cx);
+
+ let mut rust_language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_rust_servers = rust_language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name: "the-rust-language-server",
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/the-root",
+ json!({
+ "test.rs": "",
+ "package.json": "",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(rust_language));
+ });
+
+ let log_store = cx.add_model(|cx| LogStore::new(cx));
+ log_store.update(cx, |store, cx| store.add_project(&project, cx));
+
+ let _rust_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/the-root/test.rs", cx)
+ })
+ .await
+ .unwrap();
+
+ let mut language_server = fake_rust_servers.next().await.unwrap();
+ language_server
+ .receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await;
+
+ let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
+
+ language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
+ message: "hello from the server".into(),
+ typ: lsp::MessageType::INFO,
+ });
+ cx.foreground().run_until_parked();
+
+ log_view.read_with(cx, |view, cx| {
+ assert_eq!(
+ view.menu_items(cx).unwrap(),
+ &[LogMenuItem {
+ server_id: language_server.server.server_id(),
+ server_name: LanguageServerName("the-rust-language-server".into()),
+ worktree: project.read(cx).worktrees(cx).next().unwrap(),
+ rpc_trace_enabled: false,
+ rpc_trace_selected: false,
+ logs_selected: true,
+ }]
+ );
+ assert_eq!(
+ view.editor.as_ref().unwrap().read(cx).text(cx),
+ "hello from the server\n"
+ );
+ });
+}
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ client::init_settings(cx);
+ Project::init_settings(cx);
+ editor::init_settings(cx);
+ });
+}
@@ -245,10 +245,11 @@ pub struct Collaborator {
pub replica_id: ReplicaId,
}
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
pub enum Event {
LanguageServerAdded(LanguageServerId),
LanguageServerRemoved(LanguageServerId),
+ LanguageServerLog(LanguageServerId, String),
ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeAdded,
WorktreeRemoved(WorktreeId),
@@ -2454,18 +2455,23 @@ impl Project {
LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
let language_server = pending_server.task.await.log_err()?;
- let language_server = language_server
- .initialize(initialization_options)
- .await
- .log_err()?;
- let this = this.upgrade(&cx)?;
+
+ language_server
+ .on_notification::<lsp::notification::LogMessage, _>({
+ move |params, mut cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |_, cx| {
+ cx.emit(Event::LanguageServerLog(server_id, params.message))
+ });
+ }
+ }
+ })
+ .detach();
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
- let this = this.downgrade();
let adapter = adapter.clone();
move |mut params, cx| {
- let this = this;
let adapter = adapter.clone();
cx.spawn(|mut cx| async move {
adapter.process_diagnostics(&mut params).await;
@@ -2517,8 +2523,7 @@ impl Project {
// avoid stalling any language server like `gopls` which waits for a response
// to these requests when initializing.
language_server
- .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
- let this = this.downgrade();
+ .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
move |params, mut cx| async move {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, _| {
@@ -2532,12 +2537,11 @@ impl Project {
});
}
Ok(())
- }
- })
+ },
+ )
.detach();
language_server
- .on_request::<lsp::request::RegisterCapability, _, _>({
- let this = this.downgrade();
+ .on_request::<lsp::request::RegisterCapability, _, _>(
move |params, mut cx| async move {
let this = this
.upgrade(&cx)
@@ -2555,24 +2559,15 @@ impl Project {
}
}
Ok(())
- }
- })
+ },
+ )
.detach();
language_server
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
- let this = this.downgrade();
let adapter = adapter.clone();
- let language_server = language_server.clone();
move |params, cx| {
- Self::on_lsp_workspace_edit(
- this,
- params,
- server_id,
- adapter.clone(),
- language_server.clone(),
- cx,
- )
+ Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
}
})
.detach();
@@ -2582,7 +2577,6 @@ impl Project {
language_server
.on_notification::<lsp::notification::Progress, _>({
- let this = this.downgrade();
move |params, mut cx| {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
@@ -2598,6 +2592,10 @@ impl Project {
})
.detach();
+ let language_server = language_server
+ .initialize(initialization_options)
+ .await
+ .log_err()?;
language_server
.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams {
@@ -2606,6 +2604,7 @@ impl Project {
)
.ok();
+ let this = this.upgrade(&cx)?;
this.update(&mut cx, |this, cx| {
// If the language server for this key doesn't match the server id, don't store the
// server. Which will cause it to be dropped, killing the process
@@ -2640,6 +2639,8 @@ impl Project {
},
);
+ cx.emit(Event::LanguageServerAdded(server_id));
+
if let Some(project_id) = this.remote_id() {
this.client
.send(proto::StartLanguageServer {
@@ -2765,6 +2766,7 @@ impl Project {
cx.notify();
let server_state = self.language_servers.remove(&server_id);
+ cx.emit(Event::LanguageServerRemoved(server_id));
cx.spawn_weak(|this, mut cx| async move {
let mut root_path = None;
@@ -3109,12 +3111,14 @@ impl Project {
params: lsp::ApplyWorkspaceEditParams,
server_id: LanguageServerId,
adapter: Arc<CachedLspAdapter>,
- language_server: Arc<LanguageServer>,
mut cx: AsyncAppContext,
) -> Result<lsp::ApplyWorkspaceEditResponse> {
let this = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project project closed"))?;
+ let language_server = this
+ .read_with(&cx, |this, _| this.language_server_for_id(server_id))
+ .ok_or_else(|| anyhow!("language server not found"))?;
let transaction = Self::deserialize_workspace_edit(
this.clone(),
params.edit,
@@ -826,6 +826,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
let mut events = subscribe(&project, cx);
let fake_server = fake_servers.next().await.unwrap();
+ assert_eq!(
+ events.next().await.unwrap(),
+ Event::LanguageServerAdded(LanguageServerId(0)),
+ );
+
fake_server
.start_progress(format!("{}/0", progress_token))
.await;
@@ -953,6 +958,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
// Simulate the newly started server sending more diagnostics.
let fake_server = fake_servers.next().await.unwrap();
+ assert_eq!(
+ events.next().await.unwrap(),
+ Event::LanguageServerAdded(LanguageServerId(1))
+ );
fake_server.start_progress(progress_token).await;
assert_eq!(
events.next().await.unwrap(),
@@ -44,6 +44,7 @@ pub struct Theme {
pub context_menu: ContextMenu,
pub contacts_popover: ContactsPopover,
pub contact_list: ContactList,
+ pub lsp_log_menu: LspLogMenu,
pub copilot: Copilot,
pub contact_finder: ContactFinder,
pub project_panel: ProjectPanel,
@@ -244,6 +245,16 @@ pub struct ContactFinder {
pub disabled_contact_button: IconButton,
}
+#[derive(Deserialize, Default)]
+pub struct LspLogMenu {
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub header: Interactive<ContainedText>,
+ pub server: ContainedText,
+ pub item: Interactive<ContainedText>,
+ pub row_height: f32,
+}
+
#[derive(Clone, Deserialize, Default)]
pub struct TabBar {
#[serde(flatten)]
@@ -3500,7 +3500,7 @@ impl std::fmt::Debug for OpenPaths {
}
}
-pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
+pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
pub fn activate_workspace_for_project(
cx: &mut AsyncAppContext,
@@ -311,9 +311,8 @@ pub fn initialize_workspace(
toolbar.add_item(submit_feedback_button, cx);
let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
toolbar.add_item(feedback_info_text, cx);
- let lsp_log_item = cx.add_view(|_| {
- lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
- });
+ let lsp_log_item =
+ cx.add_view(|_| lsp_log::LspLogToolbarItemView::new());
toolbar.add_item(lsp_log_item, cx);
})
});
@@ -17,6 +17,7 @@ import projectSharedNotification from "./projectSharedNotification"
import tooltip from "./tooltip"
import terminal from "./terminal"
import contactList from "./contactList"
+import lspLogMenu from "./lspLogMenu"
import incomingCallNotification from "./incomingCallNotification"
import { ColorScheme } from "../theme/colorScheme"
import feedback from "./feedback"
@@ -45,6 +46,7 @@ export default function app(colorScheme: ColorScheme): Object {
contactsPopover: contactsPopover(colorScheme),
contactFinder: contactFinder(colorScheme),
contactList: contactList(colorScheme),
+ lspLogMenu: lspLogMenu(colorScheme),
search: search(colorScheme),
sharedScreen: sharedScreen(colorScheme),
updateNotification: updateNotification(colorScheme),
@@ -0,0 +1,42 @@
+import { ColorScheme } from "../theme/colorScheme"
+import { background, border, text } from "./components"
+
+export default function contactsPanel(colorScheme: ColorScheme) {
+ let layer = colorScheme.middle
+
+ return {
+ rowHeight: 30,
+ background: background(layer),
+ border: border(layer),
+ shadow: colorScheme.popoverShadow,
+ header: {
+ ...text(layer, "sans", { size: "sm" }),
+ padding: { left: 8, right: 8, top: 2, bottom: 2 },
+ cornerRadius: 6,
+ background: background(layer, "on"),
+ border: border(layer, "on", { overlay: true }),
+ hover: {
+ background: background(layer, "hovered"),
+ ...text(layer, "sans", "hovered", { size: "sm" }),
+ }
+ },
+ server: {
+ ...text(layer, "sans", { size: "sm" }),
+ padding: { left: 8, right: 8, top: 8, bottom: 8 },
+ },
+ item: {
+ ...text(layer, "sans", { size: "sm" }),
+ padding: { left: 18, right: 18, top: 2, bottom: 2 },
+ hover: {
+ background: background(layer, "hovered"),
+ ...text(layer, "sans", "hovered", { size: "sm" }),
+ },
+ active: {
+ background: background(layer, "active"),
+ },
+ activeHover: {
+ background: background(layer, "active"),
+ },
+ },
+ }
+}