mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 19:08:00 +03:00
Enhance LSP log viewer to show server logs in addition to RPC trace (#2586)
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
This commit is contained in:
commit
e3f319467a
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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 LogStoreLanguageServer {
|
||||
struct LanguageServerState {
|
||||
log_buffer: ModelHandle<Buffer>,
|
||||
rpc_state: Option<LanguageServerRpcState>,
|
||||
}
|
||||
|
||||
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,62 @@ 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.update(cx, |log_set, cx| {
|
||||
log_set.enable_logs_for_language_server(&self.project, server_id, cx)
|
||||
});
|
||||
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);
|
||||
@ -236,7 +387,28 @@ impl LspLogView {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_logging_for_server(
|
||||
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_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);
|
||||
editor.move_to_end(&Default::default(), cx);
|
||||
editor
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
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)| {
|
||||
.with_children(menu_rows.into_iter().map(|row| {
|
||||
Self::render_language_server_menu_item(
|
||||
id,
|
||||
name,
|
||||
worktree_id,
|
||||
logging_enabled,
|
||||
Some(id) == current_server_id,
|
||||
&self.project,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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,44 +640,73 @@ 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());
|
||||
enum ActivateRpcTrace {}
|
||||
|
||||
Some(
|
||||
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
|
||||
let item_style = theme.context_menu.item.style_for(state, is_selected);
|
||||
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_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(ui::checkbox_with_label::<Self, _, Self, _>(
|
||||
.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,
|
||||
logging_enabled,
|
||||
rpc_trace_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())
|
||||
)
|
||||
.flex_float(),
|
||||
)
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
.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);
|
||||
view.show_rpc_trace_for_server(id, cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
97
crates/lsp_log/src/lsp_log_tests.rs
Normal file
97
crates/lsp_log/src/lsp_log_tests.rs
Normal file
@ -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),
|
||||
|
42
styles/src/styleTree/lspLogMenu.ts
Normal file
42
styles/src/styleTree/lspLogMenu.ts
Normal file
@ -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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user