Skip to content

Commit d27291c

Browse files
committed
lsp: Reference-count log streams across views
LSP log streams can be consumed by split views and downstream clients. Treating enablement as a single value lets one consumer disable a stream that another consumer is still using. Track the streams enabled by each view and reference-count each exact server and log kind. Keep in-process view ownership separate from downstream requests, preserve RPC capture while either has an active consumer, and notify remote hosts only on the first enable and final disable. Represent log kinds independently so split views can consume logs, traces, and RPC messages without overwriting one another. Add regression coverage for both local-view and downstream release orderings. Release Notes: - Fixed closing one LSP Logs view stopping streams used by another view or downstream client
1 parent e05ca34 commit d27291c

3 files changed

Lines changed: 232 additions & 86 deletions

File tree

crates/language_tools/src/lsp_log_view.rs

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use collections::VecDeque;
1+
use collections::{HashMap, HashSet, VecDeque};
22
use edit_prediction::EditPredictionStore;
33
use editor::{Editor, EditorEvent, MultiBufferOffset, actions::MoveToEnd, scroll::Autoscroll};
44
use gpui::{
@@ -102,6 +102,7 @@ pub struct LspLogView {
102102
log_store: Entity<LogStore>,
103103
current_server_key: Option<LanguageServerLogKey>,
104104
active_entry_kind: LogKind,
105+
enabled_streams: HashMap<LanguageServerLogKey, HashSet<LogKind>>,
105106
project: Entity<Project>,
106107
focus_handle: FocusHandle,
107108
_log_store_subscriptions: Vec<Subscription>,
@@ -261,18 +262,19 @@ impl LspLogView {
261262
});
262263

263264
cx.on_release(|log_view, cx| {
264-
let project = log_view.project.downgrade();
265-
let lsp_store = log_view.project.read(cx).lsp_store().downgrade();
266-
log_view.log_store.update(cx, |log_store, cx| {
267-
for (key, state) in &log_store.language_servers {
268-
if key.is_for_project(&project, &lsp_store)
269-
&& let Some(log_kind) = state.toggled_log_kind
270-
&& let Some(log_type) = log_type(log_kind)
271-
{
272-
send_toggle_log_message(key, false, log_type, cx);
273-
}
274-
}
275-
});
265+
let enabled_streams = log_view
266+
.enabled_streams
267+
.iter()
268+
.flat_map(|(key, log_kinds)| {
269+
log_kinds
270+
.iter()
271+
.map(|log_kind| (key.clone(), *log_kind))
272+
.collect::<Vec<_>>()
273+
})
274+
.collect::<Vec<_>>();
275+
for (key, log_kind) in enabled_streams {
276+
log_view.set_stream_enabled(&key, log_kind, false, cx);
277+
}
276278
})
277279
.detach();
278280

@@ -284,6 +286,7 @@ impl LspLogView {
284286
log_store,
285287
current_server_key: None,
286288
active_entry_kind: LogKind::Logs,
289+
enabled_streams: HashMap::default(),
287290
_log_store_subscriptions: vec![
288291
model_changes_subscription,
289292
events_subscriptions,
@@ -463,6 +466,62 @@ impl LspLogView {
463466
Some(rows)
464467
}
465468

469+
fn set_stream_enabled(
470+
&mut self,
471+
key: &LanguageServerLogKey,
472+
log_kind: LogKind,
473+
enabled: bool,
474+
cx: &mut App,
475+
) {
476+
if enabled {
477+
let already_enabled = self
478+
.enabled_streams
479+
.get(key)
480+
.is_some_and(|log_kinds| log_kinds.contains(&log_kind));
481+
if already_enabled {
482+
return;
483+
}
484+
let Some(is_first) = self.log_store.update(cx, |log_store, _| {
485+
log_store.retain_view_log_stream(key, log_kind)
486+
}) else {
487+
return;
488+
};
489+
self.enabled_streams
490+
.entry(key.clone())
491+
.or_default()
492+
.insert(log_kind);
493+
if is_first && let Some(log_type) = log_type(log_kind) {
494+
send_toggle_log_message(key, true, log_type, cx);
495+
}
496+
} else {
497+
let Some(log_kinds) = self.enabled_streams.get_mut(key) else {
498+
return;
499+
};
500+
if !log_kinds.remove(&log_kind) {
501+
return;
502+
}
503+
if log_kinds.is_empty() {
504+
self.enabled_streams.remove(key);
505+
}
506+
let is_last = self
507+
.log_store
508+
.update(cx, |log_store, _| {
509+
log_store.release_view_log_stream(key, log_kind)
510+
})
511+
.unwrap_or(true);
512+
if is_last && let Some(log_type) = log_type(log_kind) {
513+
send_toggle_log_message(key, false, log_type, cx);
514+
}
515+
}
516+
}
517+
518+
fn disable_streams_for_server(&mut self, key: &LanguageServerLogKey, cx: &mut App) {
519+
let log_kinds = self.enabled_streams.get(key).cloned().unwrap_or_default();
520+
for log_kind in log_kinds {
521+
self.set_stream_enabled(key, log_kind, false, cx);
522+
}
523+
}
524+
466525
fn show_logs_for_server(
467526
&mut self,
468527
key: LanguageServerLogKey,
@@ -490,12 +549,7 @@ impl LspLogView {
490549
cx.notify();
491550
}
492551
self.editor.read(cx).focus_handle(cx).focus(window, cx);
493-
self.log_store.update(cx, |log_store, cx| {
494-
let state = log_store.get_language_server_state(&key)?;
495-
state.toggled_log_kind = Some(LogKind::Logs);
496-
send_toggle_log_message(&key, true, LogType::Log, cx);
497-
Some(())
498-
});
552+
self.set_stream_enabled(&key, LogKind::Logs, true, cx);
499553
}
500554

501555
fn update_log_level(
@@ -547,12 +601,7 @@ impl LspLogView {
547601
let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
548602
self.editor = editor;
549603
self.editor_subscriptions = editor_subscriptions;
550-
self.log_store.update(cx, |log_store, cx| {
551-
let state = log_store.get_language_server_state(&key)?;
552-
state.toggled_log_kind = Some(LogKind::Trace);
553-
send_toggle_log_message(&key, true, LogType::Trace, cx);
554-
Some(())
555-
});
604+
self.set_stream_enabled(&key, LogKind::Trace, true, cx);
556605
cx.notify();
557606
}
558607
self.editor.read(cx).focus_handle(cx).focus(window, cx);
@@ -567,7 +616,9 @@ impl LspLogView {
567616
self.toggle_rpc_trace_for_server(key.clone(), true, window, cx);
568617
let rpc_log = self.log_store.update(cx, |log_store, _| {
569618
log_store
570-
.enable_rpc_trace_for_language_server(&key)
619+
.get_language_server_state(&key)?
620+
.rpc_state
621+
.as_ref()
571622
.map(|state| log_contents(&state.rpc_messages, ()))
572623
});
573624
if let Some(rpc_log) = rpc_log {
@@ -609,17 +660,7 @@ impl LspLogView {
609660
window: &mut Window,
610661
cx: &mut Context<Self>,
611662
) {
612-
self.log_store.update(cx, |log_store, cx| {
613-
if enabled {
614-
log_store.enable_rpc_trace_for_language_server(&key);
615-
} else {
616-
log_store.disable_rpc_trace_for_language_server(&key);
617-
}
618-
619-
if log_store.language_servers.contains_key(&key) {
620-
send_toggle_log_message(&key, enabled, LogType::Rpc, cx);
621-
}
622-
});
663+
self.set_stream_enabled(&key, LogKind::Rpc, enabled, cx);
623664
if !enabled && self.current_server_key.as_ref() == Some(&key) {
624665
self.show_logs_for_server(key, window, cx);
625666
cx.notify();
@@ -704,15 +745,7 @@ impl LspLogView {
704745
self.editor_subscriptions = editor_subscriptions;
705746
cx.notify();
706747
self.editor.read(cx).focus_handle(cx).focus(window, cx);
707-
self.log_store.update(cx, |log_store, cx| {
708-
let state = log_store.get_language_server_state(&key)?;
709-
if let Some(log_kind) = state.toggled_log_kind.take()
710-
&& let Some(log_type) = log_type(log_kind)
711-
{
712-
send_toggle_log_message(&key, false, log_type, cx);
713-
}
714-
Some(())
715-
});
748+
self.disable_streams_for_server(&key, cx);
716749
}
717750
}
718751

crates/language_tools/src/lsp_log_view_tests.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use language::{
1111
use lsp::LanguageServerName;
1212
use project::{
1313
FakeFs, Project,
14-
lsp_store::log_store::{LanguageServerKind, LogKind, LogStore},
14+
lsp_store::log_store::{LanguageServerKind, LanguageServerLogKey, LogKind, LogStore},
1515
};
1616
use serde_json::json;
1717
use settings::SettingsStore;
@@ -127,6 +127,91 @@ async fn test_lsp_log_view_filters_servers_from_other_projects(cx: &mut TestAppC
127127
});
128128
}
129129

130+
#[gpui::test]
131+
async fn test_local_views_and_downstream_requests_own_rpc_streams_independently(
132+
cx: &mut TestAppContext,
133+
) {
134+
init_test(cx);
135+
136+
let fs = FakeFs::new(cx.background_executor.clone());
137+
fs.insert_tree(path!("/stream-ownership"), json!({ "test.rs": "" }))
138+
.await;
139+
let project = Project::test(fs, [path!("/stream-ownership").as_ref()], cx).await;
140+
let server_id = LanguageServerId(100);
141+
let server_key = LanguageServerLogKey::new(
142+
LanguageServerKind::Local {
143+
project: project.downgrade(),
144+
},
145+
server_id,
146+
);
147+
let log_store = cx.new(|cx| LogStore::new(false, cx));
148+
149+
log_store.update(cx, |store, cx| {
150+
store.add_language_server(
151+
server_key.kind.clone(),
152+
server_id,
153+
Some(LanguageServerName::new_static("test-server")),
154+
None,
155+
None,
156+
cx,
157+
);
158+
159+
store.toggle_lsp_logs(&server_key, true, LogKind::Rpc);
160+
assert!(
161+
store
162+
.language_servers
163+
.get(&server_key)
164+
.is_some_and(|state| state.rpc_state.is_some())
165+
);
166+
assert_eq!(
167+
store.retain_view_log_stream(&server_key, LogKind::Rpc),
168+
Some(true)
169+
);
170+
assert_eq!(
171+
store.release_view_log_stream(&server_key, LogKind::Rpc),
172+
Some(true)
173+
);
174+
assert!(
175+
store
176+
.language_servers
177+
.get(&server_key)
178+
.is_some_and(|state| state.rpc_state.is_some()),
179+
"releasing the final local view must preserve a downstream RPC request"
180+
);
181+
store.toggle_lsp_logs(&server_key, false, LogKind::Rpc);
182+
assert!(
183+
store
184+
.language_servers
185+
.get(&server_key)
186+
.is_some_and(|state| state.rpc_state.is_none())
187+
);
188+
189+
assert_eq!(
190+
store.retain_view_log_stream(&server_key, LogKind::Rpc),
191+
Some(true)
192+
);
193+
store.toggle_lsp_logs(&server_key, true, LogKind::Rpc);
194+
store.toggle_lsp_logs(&server_key, false, LogKind::Rpc);
195+
assert!(
196+
store
197+
.language_servers
198+
.get(&server_key)
199+
.is_some_and(|state| state.rpc_state.is_some()),
200+
"disabling a downstream RPC request must preserve a local view's stream"
201+
);
202+
assert_eq!(
203+
store.release_view_log_stream(&server_key, LogKind::Rpc),
204+
Some(true)
205+
);
206+
assert!(
207+
store
208+
.language_servers
209+
.get(&server_key)
210+
.is_some_and(|state| state.rpc_state.is_none())
211+
);
212+
});
213+
}
214+
130215
#[gpui::test]
131216
async fn test_lsp_log_view(cx: &mut TestAppContext) {
132217
zlog::init_test();

0 commit comments

Comments
 (0)