This commit is contained in:
Ben Kunkle 2025-08-21 10:56:51 -05:00 committed by Kirill Bulatov
parent 64b14ef848
commit 848d1101d3
9 changed files with 263 additions and 93 deletions

2
Cargo.lock generated
View file

@ -9213,6 +9213,7 @@ dependencies = [
"language",
"lsp",
"project",
"proto",
"release_channel",
"serde_json",
"settings",
@ -13500,6 +13501,7 @@ dependencies = [
"language",
"language_extension",
"language_model",
"language_tools",
"languages",
"libc",
"log",

View file

@ -24,6 +24,7 @@ itertools.workspace = true
language.workspace = true
lsp.workspace = true
project.workspace = true
proto.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true

View file

@ -1,5 +1,5 @@
mod key_context_view;
mod lsp_log;
pub mod lsp_log;
pub mod lsp_tool;
mod syntax_tree_view;

View file

@ -12,9 +12,10 @@ use lsp::{
IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
SetTraceParams, TraceValue, notification::SetTrace,
};
use project::{Project, WorktreeId, search::SearchQuery};
use project::{Project, WorktreeId, lsp_store::LanguageServerLogType, search::SearchQuery};
use std::{any::TypeId, borrow::Cow, sync::Arc};
use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
use util::ResultExt as _;
use workspace::{
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
item::{Item, ItemHandle},
@ -36,7 +37,7 @@ pub struct LogStore {
}
struct ProjectState {
_subscriptions: [gpui::Subscription; 2],
_subscriptions: [gpui::Subscription; 3],
}
trait Message: AsRef<str> {
@ -102,6 +103,7 @@ impl Message for RpcMessage {
}
pub(super) struct LanguageServerState {
project: WeakEntity<Project>,
name: Option<LanguageServerName>,
worktree_id: Option<WorktreeId>,
kind: LanguageServerKind,
@ -183,6 +185,13 @@ pub enum LogKind {
}
impl LogKind {
fn from_server_log_type(log_type: &LanguageServerLogType) -> Self {
match log_type {
LanguageServerLogType::Log(_) => Self::Logs,
LanguageServerLogType::Trace(_) => Self::Trace,
LanguageServerLogType::Rpc { .. } => Self::Rpc,
}
}
fn label(&self) -> &'static str {
match self {
LogKind::Rpc => RPC_MESSAGES,
@ -212,10 +221,11 @@ actions!(
]
);
pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
pub struct GlobalLogStore(pub WeakEntity<LogStore>);
impl Global for GlobalLogStore {}
// todo! do separate headless and local cases here: headless cares only about the downstream_client() part, NO log storage is needed
pub fn init(cx: &mut App) {
let log_store = cx.new(LogStore::new);
cx.set_global(GlobalLogStore(log_store.downgrade()));
@ -311,6 +321,7 @@ impl LogStore {
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
let weak_project = project.downgrade();
let subscription_weak_project = weak_project.clone();
self.projects.insert(
project.downgrade(),
ProjectState {
@ -356,13 +367,42 @@ impl LogStore {
this.add_language_server_log(*id, *typ, message, cx);
}
project::LanguageServerLogType::Trace(_) => {
// todo! do something with trace level
this.add_language_server_trace(*id, message, cx);
}
project::LanguageServerLogType::Rpc { received } => {
let kind = if *received {
MessageKind::Receive
} else {
MessageKind::Send
};
this.add_language_server_rpc(*id, kind, message, cx);
}
}
}
_ => {}
}
}),
cx.subscribe_self(move |_, e, cx| match e {
Event::NewServerLogEntry { id, kind, text } => {
subscription_weak_project
.update(cx, |project, cx| {
if let Some((client, project_id)) =
project.lsp_store().read(cx).downstream_client()
{
client
.send(proto::LanguageServerLog {
project_id,
language_server_id: id.to_proto(),
message: text.clone(),
log_type: Some(kind.to_proto()),
})
.log_err();
};
})
.ok();
}
}),
],
},
);
@ -382,6 +422,7 @@ impl LogStore {
name: Option<LanguageServerName>,
worktree_id: Option<WorktreeId>,
server: Option<Arc<LanguageServer>>,
project: WeakEntity<Project>,
cx: &mut Context<Self>,
) -> Option<&mut LanguageServerState> {
let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
@ -390,6 +431,7 @@ impl LogStore {
name: None,
worktree_id: None,
kind,
project,
rpc_state: None,
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
@ -429,17 +471,21 @@ impl LogStore {
let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.log_messages;
Self::add_language_server_message(
if let Some(new_message) = Self::push_new_message(
log_lines,
id,
LogMessage {
message: message.trim_end().to_string(),
typ,
},
language_server_state.log_level,
LogKind::Logs,
cx,
);
) {
cx.emit(Event::NewServerLogEntry {
id,
kind: LanguageServerLogType::Log(typ),
text: new_message,
});
}
Some(())
}
@ -452,38 +498,81 @@ impl LogStore {
let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.trace_messages;
Self::add_language_server_message(
if let Some(new_message) = Self::push_new_message(
log_lines,
id,
TraceMessage {
message: message.trim().to_string(),
},
(),
LogKind::Trace,
cx,
);
) {
cx.emit(Event::NewServerLogEntry {
id,
// todo! Ben, fix this here too!
kind: LanguageServerLogType::Trace(project::lsp_store::TraceLevel::Verbose),
text: new_message,
});
}
Some(())
}
fn add_language_server_message<T: Message>(
fn push_new_message<T: Message>(
log_lines: &mut VecDeque<T>,
id: LanguageServerId,
message: T,
current_severity: <T as Message>::Level,
kind: LogKind,
cx: &mut Context<Self>,
) {
) -> Option<String> {
while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front();
}
let text = message.as_ref().to_string();
let visible = message.should_include(current_severity);
log_lines.push_back(message);
if visible {
cx.emit(Event::NewServerLogEntry { id, kind, text });
cx.notify();
let re = visible.then(|| message.as_ref().to_string());
log_lines.push_back(message);
re
}
fn add_language_server_rpc(
&mut self,
language_server_id: LanguageServerId,
kind: MessageKind,
message: &str,
cx: &mut Context<'_, LogStore>,
) {
let Some(state) = self
.get_language_server_state(language_server_id)
.and_then(|state| state.rpc_state.as_mut())
else {
return;
};
let rpc_log_lines = &mut state.rpc_messages;
if state.last_message_kind != Some(kind) {
while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
let line_before_message = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
rpc_log_lines.push_back(RpcMessage {
message: line_before_message.to_string(),
});
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
kind: LanguageServerLogType::Rpc {
received: kind == MessageKind::Receive,
},
text: line_before_message.to_string(),
});
}
while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
rpc_log_lines.push_back(RpcMessage {
message: message.trim().to_owned(),
});
}
fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
@ -520,7 +609,7 @@ impl LogStore {
})
}
fn enable_rpc_trace_for_language_server(
pub fn enable_rpc_trace_for_language_server(
&mut self,
server_id: LanguageServerId,
) -> Option<&mut LanguageServerRpcState> {
@ -663,47 +752,19 @@ impl LogStore {
}
};
let state = self
.get_language_server_state(language_server_id)?
.rpc_state
.as_mut()?;
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
};
let rpc_log_lines = &mut state.rpc_messages;
if state.last_message_kind != Some(kind) {
while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
let line_before_message = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
rpc_log_lines.push_back(RpcMessage {
message: line_before_message.to_string(),
});
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
kind: LogKind::Rpc,
text: line_before_message.to_string(),
});
}
while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
let message = message.trim();
rpc_log_lines.push_back(RpcMessage {
message: message.to_string(),
});
self.add_language_server_rpc(language_server_id, kind, message, cx);
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
kind: LogKind::Rpc,
text: message.to_string(),
kind: LanguageServerLogType::Rpc {
received: is_received,
},
text: message.to_owned(),
});
cx.notify();
Some(())
@ -757,7 +818,7 @@ impl LspLogView {
move |log_view, _, e, window, cx| match e {
Event::NewServerLogEntry { id, kind, text } => {
if log_view.current_server_id == Some(*id)
&& *kind == log_view.active_entry_kind
&& LogKind::from_server_log_type(kind) == log_view.active_entry_kind
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
@ -1075,6 +1136,21 @@ impl LspLogView {
} else {
log_store.disable_rpc_trace_for_language_server(server_id);
}
if let Some(server_state) = log_store.language_servers.get(server_id) {
server_state
.project
.update(cx, |project, cx| {
if let Some((client, project)) =
project.lsp_store().read(cx).upstream_client()
{
// todo! client.send a new proto message to propagate the enabled
// !!!! we have to have a handler on both headless and normal projects
// that handler has to touch the Global<LspLog> and amend the sending bit
}
})
.ok();
};
});
if !enabled && Some(server_id) == self.current_server_id {
self.show_logs_for_server(server_id, window, cx);
@ -1113,6 +1189,8 @@ impl LspLogView {
window: &mut Window,
cx: &mut Context<Self>,
) {
// todo! there's no language server for the remote case, hence no server info!
// BUT we do have the capabilities info within the LspStore.lsp_server_capabilities
let lsp_store = self.project.read(cx).lsp_store();
let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
return;
@ -1737,7 +1815,7 @@ impl LspLogToolbarItemView {
pub enum Event {
NewServerLogEntry {
id: LanguageServerId,
kind: LogKind,
kind: LanguageServerLogType,
text: String,
},
}

View file

@ -122,8 +122,7 @@ impl LanguageServerState {
let lsp_logs = cx
.try_global::<GlobalLogStore>()
.and_then(|lsp_logs| lsp_logs.0.upgrade());
let lsp_store = self.lsp_store.upgrade();
let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
let Some(lsp_logs) = lsp_logs else {
return menu;
};
@ -210,10 +209,7 @@ impl LanguageServerState {
};
let server_selector = server_info.server_selector();
// TODO currently, Zed remote does not work well with the LSP logs
// https://github.com/zed-industries/zed/issues/28557
let has_logs = lsp_store.read(cx).as_local().is_some()
&& lsp_logs.read(cx).has_server_logs(&server_selector);
let has_logs = lsp_logs.read(cx).has_server_logs(&server_selector);
let status_color = server_info
.binary_status

View file

@ -977,7 +977,13 @@ impl LocalLspStore {
this.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerLog(
server_id,
LanguageServerLogType::Trace(params.verbose),
// todo! store verbose info on Verbose
LanguageServerLogType::Trace(
params
.verbose
.map(|_verbose_info| TraceLevel::Verbose)
.unwrap_or(TraceLevel::Messages),
),
params.message,
));
})
@ -12168,6 +12174,10 @@ impl LspStore {
let data = self.lsp_code_lens.get_mut(&buffer_id)?;
Some(data.update.take()?.1)
}
pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> {
self.downstream_client.clone()
}
}
// Registration with registerOptions as null, should fallback to true.
@ -12674,48 +12684,92 @@ impl PartialEq for LanguageServerPromptRequest {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceLevel {
Off,
Messages,
Verbose,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LanguageServerLogType {
Log(MessageType),
Trace(Option<String>),
Trace(TraceLevel),
Rpc { received: bool },
}
impl LanguageServerLogType {
pub fn to_proto(&self) -> proto::language_server_log::LogType {
match self {
Self::Log(log_type) => {
let message_type = match *log_type {
MessageType::ERROR => 1,
MessageType::WARNING => 2,
MessageType::INFO => 3,
MessageType::LOG => 4,
use proto::log_message::LogLevel;
let level = match *log_type {
MessageType::ERROR => LogLevel::Error,
MessageType::WARNING => LogLevel::Warning,
MessageType::INFO => LogLevel::Info,
MessageType::LOG => LogLevel::Log,
other => {
log::warn!("Unknown lsp log message type: {:?}", other);
4
log::warn!("Unknown lsp log message type: {other:?}");
LogLevel::Log
}
};
proto::language_server_log::LogType::LogMessageType(message_type)
}
Self::Trace(message) => {
proto::language_server_log::LogType::LogTrace(proto::LspLogTrace {
message: message.clone(),
proto::language_server_log::LogType::Log(proto::LogMessage {
level: level as i32,
})
}
Self::Trace(trace_level) => {
use proto::trace_message;
let level = match trace_level {
TraceLevel::Off => trace_message::TraceLevel::Off,
TraceLevel::Messages => trace_message::TraceLevel::Messages,
TraceLevel::Verbose => trace_message::TraceLevel::Verbose,
};
proto::language_server_log::LogType::Trace(proto::TraceMessage {
level: level as i32,
})
}
Self::Rpc { received } => {
let kind = if *received {
proto::rpc_message::Kind::Received
} else {
proto::rpc_message::Kind::Sent
};
let kind = kind as i32;
proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind })
}
}
}
pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self {
use proto::log_message::LogLevel;
use proto::rpc_message;
use proto::trace_message;
match log_type {
proto::language_server_log::LogType::LogMessageType(message_type) => {
Self::Log(match message_type {
1 => MessageType::ERROR,
2 => MessageType::WARNING,
3 => MessageType::INFO,
4 => MessageType::LOG,
_ => MessageType::LOG,
})
}
proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message),
proto::language_server_log::LogType::Log(message_type) => Self::Log(
match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) {
LogLevel::Error => MessageType::ERROR,
LogLevel::Warning => MessageType::WARNING,
LogLevel::Info => MessageType::INFO,
LogLevel::Log => MessageType::LOG,
},
),
proto::language_server_log::LogType::Trace(trace) => Self::Trace(
match trace_message::TraceLevel::from_i32(trace.level)
.unwrap_or(trace_message::TraceLevel::Messages)
{
trace_message::TraceLevel::Off => TraceLevel::Off,
trace_message::TraceLevel::Messages => TraceLevel::Messages,
trace_message::TraceLevel::Verbose => TraceLevel::Verbose,
},
),
proto::language_server_log::LogType::Rpc(message) => Self::Rpc {
received: match rpc_message::Kind::from_i32(message.kind)
.unwrap_or(rpc_message::Kind::Received)
{
rpc_message::Kind::Received => true,
rpc_message::Kind::Sent => false,
},
},
}
}
}

View file

@ -610,11 +610,42 @@ message ServerMetadataUpdated {
message LanguageServerLog {
uint64 project_id = 1;
uint64 language_server_id = 2;
string message = 3;
oneof log_type {
uint32 log_message_type = 3;
LspLogTrace log_trace = 4;
LogMessage log = 4;
TraceMessage trace = 5;
RpcMessage rpc = 6;
}
}
message LogMessage {
LogLevel level = 1;
enum LogLevel {
LOG = 0;
INFO = 1;
WARNING = 2;
ERROR = 3;
}
}
message TraceMessage {
TraceLevel level = 1;
enum TraceLevel {
OFF = 0;
MESSAGES = 1;
VERBOSE = 2;
}
}
message RpcMessage {
Kind kind = 1;
enum Kind {
RECEIVED = 0;
SENT = 1;
}
string message = 5;
}
message LspLogTrace {

View file

@ -43,6 +43,7 @@ gpui_tokio.workspace = true
http_client.workspace = true
language.workspace = true
language_extension.workspace = true
language_tools.workspace = true
languages.workspace = true
log.workspace = true
lsp.workspace = true

View file

@ -65,6 +65,13 @@ impl HeadlessProject {
settings::init(cx);
language::init(cx);
project::Project::init_settings(cx);
// todo! what to do with the RPC log spam?
// if we have not enabled RPC logging on the remote client, we do not need these
//
// Maybe, add another RPC message, proto::ToggleRpcLogging(bool)
// and send it into the upstream client from the remotes, so that the local/headless counterpart
// can access this Global<LspLog> and toggle the spam send
language_tools::lsp_log::init(cx);
}
pub fn new(