Skip to repository content1384 lines · 56.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:52:02.939Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
lsp_log_view.rs
1use collections::VecDeque;
2use edit_prediction::EditPredictionStore;
3use editor::{Editor, EditorEvent, MultiBufferOffset, actions::MoveToEnd, scroll::Autoscroll};
4use gpui::{
5 Anchor, App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement,
6 Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
7};
8use itertools::Itertools as _;
9use language::{LanguageServerId, language_settings::SoftWrap};
10use lsp::{
11 LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, SetTraceParams,
12 TraceValue, notification::SetTrace,
13};
14use project::{
15 LanguageServerStatus, Project,
16 lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message},
17 search::SearchQuery,
18};
19use proto::toggle_lsp_logs::LogType;
20use settings::SeedQuerySetting;
21use std::{any::TypeId, borrow::Cow, sync::Arc};
22use ui::{Checkbox, ContextMenu, PopoverMenu, ToggleState, prelude::*};
23use util::ResultExt as _;
24use workspace::{
25 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
26 item::{Item, ItemHandle},
27 searchable::{Direction, SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},
28};
29
30use crate::get_or_create_tool;
31
32pub fn open(
33 log_store: &Entity<LogStore>,
34 workspace: WeakEntity<Workspace>,
35 server: LanguageServerSelector,
36 window: &mut Window,
37 cx: &mut App,
38) {
39 log_store.update(cx, |_, cx| {
40 cx.spawn_in(window, async move |log_store, cx| {
41 let Some(log_store) = log_store.upgrade() else {
42 return;
43 };
44 workspace
45 .update_in(cx, |workspace, window, cx| {
46 let project = workspace.project().clone();
47 let tool_log_store = log_store.clone();
48 let log_view = get_or_create_tool(
49 workspace,
50 SplitDirection::Right,
51 window,
52 cx,
53 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
54 );
55 log_view.update(cx, |log_view, cx| {
56 let server_id = match server {
57 LanguageServerSelector::Id(id) => Some(id),
58 LanguageServerSelector::Name(name) => {
59 log_store.read(cx).language_servers.iter().find_map(
60 |(id, state)| {
61 if state.name.as_ref() == Some(&name) {
62 Some(*id)
63 } else {
64 None
65 }
66 },
67 )
68 }
69 };
70 if let Some(server_id) = server_id {
71 log_view.show_logs_for_server(server_id, window, cx);
72 }
73 });
74 })
75 .ok();
76 })
77 .detach();
78 })
79}
80
81pub struct LspLogView {
82 pub(crate) editor: Entity<Editor>,
83 editor_subscriptions: Vec<Subscription>,
84 log_store: Entity<LogStore>,
85 current_server_id: Option<LanguageServerId>,
86 active_entry_kind: LogKind,
87 project: Entity<Project>,
88 focus_handle: FocusHandle,
89 _log_store_subscriptions: Vec<Subscription>,
90}
91
92pub struct LspLogToolbarItemView {
93 log_view: Option<Entity<LspLogView>>,
94 _log_view_subscription: Option<Subscription>,
95}
96
97#[derive(Clone, Debug, PartialEq)]
98pub(crate) struct LogMenuItem {
99 pub server_id: LanguageServerId,
100 pub server_name: LanguageServerName,
101 pub worktree_root_name: String,
102 pub rpc_trace_enabled: bool,
103 pub selected_entry: LogKind,
104 pub trace_level: lsp::TraceValue,
105 pub server_kind: LanguageServerKind,
106}
107
108actions!(
109 dev,
110 [
111 /// Opens the language server protocol logs viewer.
112 OpenLanguageServerLogs
113 ]
114);
115
116pub fn init(on_headless_host: bool, cx: &mut App) {
117 let log_store = log_store::init(on_headless_host, cx);
118
119 cx.observe_new(move |workspace: &mut Workspace, _, cx| {
120 log_store.update(cx, |store, cx| {
121 store.add_project(workspace.project(), cx);
122 });
123
124 let log_store = log_store.clone();
125 workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
126 let log_store = log_store.clone();
127 let project = workspace.project().clone();
128 get_or_create_tool(
129 workspace,
130 SplitDirection::Right,
131 window,
132 cx,
133 move |window, cx| LspLogView::new(project, log_store, window, cx),
134 );
135 });
136 })
137 .detach();
138}
139
140impl LspLogView {
141 pub fn new(
142 project: Entity<Project>,
143 log_store: Entity<LogStore>,
144 window: &mut Window,
145 cx: &mut Context<Self>,
146 ) -> Self {
147 let server_id = log_store
148 .read(cx)
149 .language_servers
150 .iter()
151 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
152 .map(|(id, _)| *id);
153
154 let weak_project = project.downgrade();
155 let model_changes_subscription =
156 cx.observe_in(&log_store, window, move |this, store, window, cx| {
157 let first_server_id_for_project =
158 store.read(cx).server_ids_for_project(&weak_project).next();
159 if let Some(current_lsp) = this.current_server_id {
160 if !store.read(cx).language_servers.contains_key(¤t_lsp)
161 && let Some(server_id) = first_server_id_for_project
162 {
163 match this.active_entry_kind {
164 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
165 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
166 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
167 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
168 }
169 }
170 } else if let Some(server_id) = first_server_id_for_project {
171 match this.active_entry_kind {
172 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
173 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
174 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
175 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
176 }
177 }
178
179 cx.notify();
180 });
181
182 let events_subscriptions = cx.subscribe_in(
183 &log_store,
184 window,
185 move |log_view, _, e, window, cx| match e {
186 Event::NewServerLogEntry { id, kind, text } => {
187 if log_view.current_server_id == Some(*id)
188 && LogKind::from_server_log_type(kind) == log_view.active_entry_kind
189 {
190 log_view.editor.update(cx, |editor, cx| {
191 editor.set_read_only(false);
192 let last_offset = editor.buffer().read(cx).len(cx);
193 let newest_cursor_is_at_end = editor
194 .selections
195 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
196 .start
197 >= last_offset;
198 editor.edit(
199 vec![
200 (last_offset..last_offset, text.as_str()),
201 (last_offset..last_offset, "\n"),
202 ],
203 cx,
204 );
205 if text.len() > 1024 {
206 let b = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
207 let fold_offset =
208 b.as_rope().ceil_char_boundary(last_offset.0 + 1024);
209 editor.fold_ranges(
210 vec![
211 MultiBufferOffset(fold_offset)
212 ..MultiBufferOffset(b.as_rope().len()),
213 ],
214 false,
215 window,
216 cx,
217 );
218 }
219
220 if newest_cursor_is_at_end {
221 editor.request_autoscroll(Autoscroll::bottom(), cx);
222 }
223 editor.set_read_only(true);
224 });
225 }
226 }
227 },
228 );
229 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
230
231 let focus_handle = cx.focus_handle();
232 let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
233 window.focus(&log_view.editor.focus_handle(cx), cx);
234 });
235
236 cx.on_release(|log_view, cx| {
237 log_view.log_store.update(cx, |log_store, cx| {
238 for (server_id, state) in &log_store.language_servers {
239 if let Some(log_kind) = state.toggled_log_kind {
240 if let Some(log_type) = log_type(log_kind) {
241 send_toggle_log_message(state, *server_id, false, log_type, cx);
242 }
243 }
244 }
245 });
246 })
247 .detach();
248
249 let mut lsp_log_view = Self {
250 focus_handle,
251 editor,
252 editor_subscriptions,
253 project,
254 log_store,
255 current_server_id: None,
256 active_entry_kind: LogKind::Logs,
257 _log_store_subscriptions: vec![
258 model_changes_subscription,
259 events_subscriptions,
260 focus_subscription,
261 ],
262 };
263 if let Some(server_id) = server_id {
264 lsp_log_view.show_logs_for_server(server_id, window, cx);
265 }
266 lsp_log_view
267 }
268
269 fn editor_for_logs(
270 log_contents: String,
271 window: &mut Window,
272 cx: &mut Context<Self>,
273 ) -> (Entity<Editor>, Vec<Subscription>) {
274 let editor = initialize_new_editor(log_contents, true, window, cx);
275 let editor_subscription = cx.subscribe(
276 &editor,
277 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
278 );
279 let search_subscription = cx.subscribe(
280 &editor,
281 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
282 );
283 (editor, vec![editor_subscription, search_subscription])
284 }
285
286 fn editor_for_server_info(
287 info: ServerInfo,
288 window: &mut Window,
289 cx: &mut Context<Self>,
290 ) -> (Entity<Editor>, Vec<Subscription>) {
291 let server_info = format!(
292 "* Server: {NAME} (id {ID})
293
294* Version: {VERSION}
295
296* Binary: {BINARY}
297
298* Registered workspace folders:
299{WORKSPACE_FOLDERS}
300
301* Capabilities: {CAPABILITIES}
302
303* Configuration: {CONFIGURATION}",
304 NAME = info.status.name,
305 ID = info.id,
306 VERSION = info
307 .status
308 .server_version
309 .as_ref()
310 .map(|version| version.as_ref())
311 .unwrap_or("Unknown"),
312 BINARY = info
313 .status
314 .binary
315 .as_ref()
316 .map_or_else(|| "Unknown".to_string(), |binary| format!("{:#?}", binary)),
317 WORKSPACE_FOLDERS = info
318 .status
319 .workspace_folders
320 .iter()
321 .filter_map(|uri| uri.to_file_path().ok())
322 .map(|path| path.to_string_lossy().into_owned())
323 .join(", "),
324 CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
325 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
326 CONFIGURATION = info
327 .status
328 .configuration
329 .map(|configuration| serde_json::to_string_pretty(&configuration))
330 .transpose()
331 .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}")))
332 .unwrap_or_else(|| "Unknown".to_string()),
333 );
334 let editor = initialize_new_editor(server_info, false, window, cx);
335 let editor_subscription = cx.subscribe(
336 &editor,
337 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
338 );
339 let search_subscription = cx.subscribe(
340 &editor,
341 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
342 );
343 (editor, vec![editor_subscription, search_subscription])
344 }
345 pub(crate) fn try_ensure_copilot_for_project(&self, cx: &mut App) {
346 self.log_store.update(cx, |this, cx| {
347 let copilot = EditPredictionStore::try_global(cx)
348 .and_then(|store| store.read(cx).copilot_for_project(&self.project))?;
349 let server = copilot.read(cx).language_server()?.clone();
350 let log_subscription = this.copilot_state_for_project(&self.project.downgrade());
351 if let Some(subscription_slot @ None) = log_subscription {
352 let weak_lsp_store = cx.weak_entity();
353 let server_id = server.server_id();
354
355 let name = LanguageServerName::new_static("copilot");
356 *subscription_slot =
357 Some(server.on_notification::<lsp::notification::LogMessage, _>(
358 move |params, cx| {
359 weak_lsp_store
360 .update(cx, |lsp_store, cx| {
361 lsp_store.add_language_server_log(
362 server_id,
363 MessageType::LOG,
364 ¶ms.message,
365 cx,
366 );
367 })
368 .ok();
369 },
370 ));
371 this.add_language_server(
372 LanguageServerKind::Global,
373 server.server_id(),
374 Some(name),
375 None,
376 Some(server.clone()),
377 cx,
378 );
379 }
380
381 Some(())
382 });
383 }
384 pub(crate) fn menu_items(&self, cx: &mut App) -> Option<Vec<LogMenuItem>> {
385 self.try_ensure_copilot_for_project(cx);
386 let log_store = self.log_store.read(cx);
387
388 let unknown_server = LanguageServerName::new_static("unknown server");
389
390 let mut rows = log_store
391 .language_servers
392 .iter()
393 .map(|(server_id, state)| match &state.kind {
394 LanguageServerKind::Local { .. }
395 | LanguageServerKind::Remote { .. }
396 | LanguageServerKind::LocalSsh { .. } => {
397 let worktree_root_name = state
398 .worktree_id
399 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
400 .map(|worktree| worktree.read(cx).root_name_str().to_string())
401 .unwrap_or_else(|| "Unknown worktree".to_string());
402
403 LogMenuItem {
404 server_id: *server_id,
405 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
406 server_kind: state.kind.clone(),
407 worktree_root_name,
408 rpc_trace_enabled: state.rpc_state.is_some(),
409 selected_entry: self.active_entry_kind,
410 trace_level: lsp::TraceValue::Off,
411 }
412 }
413
414 LanguageServerKind::Global => LogMenuItem {
415 server_id: *server_id,
416 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
417 server_kind: state.kind.clone(),
418 worktree_root_name: "supplementary".to_string(),
419 rpc_trace_enabled: state.rpc_state.is_some(),
420 selected_entry: self.active_entry_kind,
421 trace_level: lsp::TraceValue::Off,
422 },
423 })
424 .chain(
425 self.project
426 .read(cx)
427 .supplementary_language_servers(cx)
428 .filter_map(|(server_id, name)| {
429 let state = log_store.language_servers.get(&server_id)?;
430 Some(LogMenuItem {
431 server_id,
432 server_name: name,
433 server_kind: state.kind.clone(),
434 worktree_root_name: "supplementary".to_string(),
435 rpc_trace_enabled: state.rpc_state.is_some(),
436 selected_entry: self.active_entry_kind,
437 trace_level: lsp::TraceValue::Off,
438 })
439 }),
440 )
441 .collect::<Vec<_>>();
442 rows.sort_by_key(|row| row.server_id);
443 rows.dedup_by_key(|row| row.server_id);
444 Some(rows)
445 }
446
447 fn show_logs_for_server(
448 &mut self,
449 server_id: LanguageServerId,
450 window: &mut Window,
451 cx: &mut Context<Self>,
452 ) {
453 let typ = self
454 .log_store
455 .read(cx)
456 .language_servers
457 .get(&server_id)
458 .map(|v| v.log_level)
459 .unwrap_or(MessageType::LOG);
460 let log_contents = self
461 .log_store
462 .read(cx)
463 .server_logs(server_id)
464 .map(|v| log_contents(v, typ));
465 if let Some(log_contents) = log_contents {
466 self.current_server_id = Some(server_id);
467 self.active_entry_kind = LogKind::Logs;
468 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
469 self.editor = editor;
470 self.editor_subscriptions = editor_subscriptions;
471 cx.notify();
472 }
473 self.editor.read(cx).focus_handle(cx).focus(window, cx);
474 self.log_store.update(cx, |log_store, cx| {
475 let state = log_store.get_language_server_state(server_id)?;
476 state.toggled_log_kind = Some(LogKind::Logs);
477 send_toggle_log_message(state, server_id, true, LogType::Log, cx);
478 Some(())
479 });
480 }
481
482 fn update_log_level(
483 &self,
484 server_id: LanguageServerId,
485 level: MessageType,
486 window: &mut Window,
487 cx: &mut Context<Self>,
488 ) {
489 let log_contents = self.log_store.update(cx, |this, _| {
490 if let Some(state) = this.get_language_server_state(server_id) {
491 state.log_level = level;
492 }
493
494 this.server_logs(server_id).map(|v| log_contents(v, level))
495 });
496
497 if let Some(log_contents) = log_contents {
498 self.editor.update(cx, |editor, cx| {
499 editor.set_text(log_contents, window, cx);
500 editor.move_to_end(&MoveToEnd, window, cx);
501 });
502 cx.notify();
503 }
504
505 self.editor.read(cx).focus_handle(cx).focus(window, cx);
506 }
507
508 fn show_trace_for_server(
509 &mut self,
510 server_id: LanguageServerId,
511 window: &mut Window,
512 cx: &mut Context<Self>,
513 ) {
514 let trace_level = self
515 .log_store
516 .update(cx, |log_store, _| {
517 Some(log_store.get_language_server_state(server_id)?.trace_level)
518 })
519 .unwrap_or(TraceValue::Messages);
520 let log_contents = self
521 .log_store
522 .read(cx)
523 .server_trace(server_id)
524 .map(|v| log_contents(v, trace_level));
525 if let Some(log_contents) = log_contents {
526 self.current_server_id = Some(server_id);
527 self.active_entry_kind = LogKind::Trace;
528 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
529 self.editor = editor;
530 self.editor_subscriptions = editor_subscriptions;
531 self.log_store.update(cx, |log_store, cx| {
532 let state = log_store.get_language_server_state(server_id)?;
533 state.toggled_log_kind = Some(LogKind::Trace);
534 send_toggle_log_message(state, server_id, true, LogType::Trace, cx);
535 Some(())
536 });
537 cx.notify();
538 }
539 self.editor.read(cx).focus_handle(cx).focus(window, cx);
540 }
541
542 fn show_rpc_trace_for_server(
543 &mut self,
544 server_id: LanguageServerId,
545 window: &mut Window,
546 cx: &mut Context<Self>,
547 ) {
548 self.toggle_rpc_trace_for_server(server_id, true, window, cx);
549 let rpc_log = self.log_store.update(cx, |log_store, _| {
550 log_store
551 .enable_rpc_trace_for_language_server(server_id)
552 .map(|state| log_contents(&state.rpc_messages, ()))
553 });
554 if let Some(rpc_log) = rpc_log {
555 self.current_server_id = Some(server_id);
556 self.active_entry_kind = LogKind::Rpc;
557 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
558 let language = self.project.read(cx).languages().language_for_name("JSON");
559 editor
560 .read(cx)
561 .buffer()
562 .read(cx)
563 .as_singleton()
564 .expect("log buffer should be a singleton")
565 .update(cx, |_, cx| {
566 cx.spawn({
567 let buffer = cx.entity();
568 async move |_, cx| {
569 let language = language.await.ok();
570 buffer.update(cx, |buffer, cx| {
571 buffer.set_language(language, cx);
572 });
573 }
574 })
575 .detach();
576 });
577
578 self.editor = editor;
579 self.editor_subscriptions = editor_subscriptions;
580 cx.notify();
581 }
582
583 self.editor.read(cx).focus_handle(cx).focus(window, cx);
584 }
585
586 fn toggle_rpc_trace_for_server(
587 &mut self,
588 server_id: LanguageServerId,
589 enabled: bool,
590 window: &mut Window,
591 cx: &mut Context<Self>,
592 ) {
593 self.log_store.update(cx, |log_store, cx| {
594 if enabled {
595 log_store.enable_rpc_trace_for_language_server(server_id);
596 } else {
597 log_store.disable_rpc_trace_for_language_server(server_id);
598 }
599
600 if let Some(server_state) = log_store.language_servers.get(&server_id) {
601 send_toggle_log_message(server_state, server_id, enabled, LogType::Rpc, cx);
602 };
603 });
604 if !enabled && Some(server_id) == self.current_server_id {
605 self.show_logs_for_server(server_id, window, cx);
606 cx.notify();
607 }
608 }
609
610 fn update_trace_level(
611 &self,
612 server_id: LanguageServerId,
613 level: TraceValue,
614 cx: &mut Context<Self>,
615 ) {
616 if let Some(server) = self
617 .project
618 .read(cx)
619 .lsp_store()
620 .read(cx)
621 .language_server_for_id(server_id)
622 {
623 self.log_store.update(cx, |this, _| {
624 if let Some(state) = this.get_language_server_state(server_id) {
625 state.trace_level = level;
626 }
627 });
628
629 server
630 .notify::<SetTrace>(SetTraceParams { value: level })
631 .ok();
632 }
633 }
634
635 fn show_server_info(
636 &mut self,
637 server_id: LanguageServerId,
638 window: &mut Window,
639 cx: &mut Context<Self>,
640 ) {
641 let Some(server_info) = self
642 .project
643 .read(cx)
644 .lsp_store()
645 .update(cx, |lsp_store, _| {
646 lsp_store
647 .language_server_for_id(server_id)
648 .as_ref()
649 .map(|language_server| ServerInfo::new(language_server))
650 .or_else(move || {
651 let capabilities =
652 lsp_store.lsp_server_capabilities.get(&server_id)?.clone();
653 let status = lsp_store.language_server_statuses.get(&server_id)?.clone();
654
655 Some(ServerInfo {
656 id: server_id,
657 capabilities,
658 status,
659 })
660 })
661 })
662 else {
663 return;
664 };
665 self.current_server_id = Some(server_id);
666 self.active_entry_kind = LogKind::ServerInfo;
667 let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx);
668 self.editor = editor;
669 self.editor_subscriptions = editor_subscriptions;
670 cx.notify();
671 self.editor.read(cx).focus_handle(cx).focus(window, cx);
672 self.log_store.update(cx, |log_store, cx| {
673 let state = log_store.get_language_server_state(server_id)?;
674 if let Some(log_kind) = state.toggled_log_kind.take() {
675 if let Some(log_type) = log_type(log_kind) {
676 send_toggle_log_message(state, server_id, false, log_type, cx);
677 }
678 };
679 Some(())
680 });
681 }
682}
683
684fn log_type(log_kind: LogKind) -> Option<LogType> {
685 match log_kind {
686 LogKind::Rpc => Some(LogType::Rpc),
687 LogKind::Trace => Some(LogType::Trace),
688 LogKind::Logs => Some(LogType::Log),
689 LogKind::ServerInfo => None,
690 }
691}
692
693fn send_toggle_log_message(
694 server_state: &log_store::LanguageServerState,
695 server_id: LanguageServerId,
696 enabled: bool,
697 log_type: LogType,
698 cx: &mut App,
699) {
700 if let LanguageServerKind::Remote { project } = &server_state.kind {
701 project
702 .update(cx, |project, cx| {
703 if let Some((client, project_id)) = project.lsp_store().read(cx).upstream_client() {
704 client
705 .send(proto::ToggleLspLogs {
706 project_id,
707 log_type: log_type as i32,
708 server_id: server_id.to_proto(),
709 enabled,
710 })
711 .log_err();
712 }
713 })
714 .ok();
715 }
716}
717
718fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
719 lines
720 .iter()
721 .filter(|message| message.should_include(level))
722 .flat_map(|message| [message.as_ref(), "\n"])
723 .collect()
724}
725
726impl Render for LspLogView {
727 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
728 self.editor.update(cx, |editor, cx| {
729 editor.render(window, cx).into_any_element()
730 })
731 }
732}
733
734impl Focusable for LspLogView {
735 fn focus_handle(&self, _: &App) -> FocusHandle {
736 self.focus_handle.clone()
737 }
738}
739
740impl Item for LspLogView {
741 type Event = EditorEvent;
742
743 fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
744 Editor::to_item_events(event, f)
745 }
746
747 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
748 "LSP Logs".into()
749 }
750
751 fn telemetry_event_text(&self) -> Option<&'static str> {
752 None
753 }
754
755 fn as_searchable(
756 &self,
757 handle: &Entity<Self>,
758 _: &App,
759 ) -> Option<Box<dyn SearchableItemHandle>> {
760 Some(Box::new(handle.clone()))
761 }
762
763 fn act_as_type<'a>(
764 &'a self,
765 type_id: TypeId,
766 self_handle: &'a Entity<Self>,
767 _: &'a App,
768 ) -> Option<gpui::AnyEntity> {
769 if type_id == TypeId::of::<Self>() {
770 Some(self_handle.clone().into())
771 } else if type_id == TypeId::of::<Editor>() {
772 Some(self.editor.clone().into())
773 } else {
774 None
775 }
776 }
777
778 fn can_split(&self) -> bool {
779 true
780 }
781
782 fn clone_on_split(
783 &self,
784 _workspace_id: Option<WorkspaceId>,
785 window: &mut Window,
786 cx: &mut Context<Self>,
787 ) -> Task<Option<Entity<Self>>>
788 where
789 Self: Sized,
790 {
791 Task::ready(Some(cx.new(|cx| {
792 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
793 if let Some(server_id) = self.current_server_id {
794 match self.active_entry_kind {
795 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
796 LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
797 LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
798 LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
799 }
800 }
801 new_view
802 })))
803 }
804}
805
806impl SearchableItem for LspLogView {
807 type Match = <Editor as SearchableItem>::Match;
808
809 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
810 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
811 }
812
813 fn update_matches(
814 &mut self,
815 matches: &[Self::Match],
816 active_match_index: Option<usize>,
817 token: SearchToken,
818 window: &mut Window,
819 cx: &mut Context<Self>,
820 ) {
821 self.editor.update(cx, |e, cx| {
822 e.update_matches(matches, active_match_index, token, window, cx)
823 })
824 }
825
826 fn query_suggestion(
827 &mut self,
828 seed_query_override: Option<SeedQuerySetting>,
829 window: &mut Window,
830 cx: &mut Context<Self>,
831 ) -> String {
832 self.editor.update(cx, |e, cx| {
833 e.query_suggestion(seed_query_override, window, cx)
834 })
835 }
836
837 fn activate_match(
838 &mut self,
839 index: usize,
840 matches: &[Self::Match],
841 token: SearchToken,
842 window: &mut Window,
843 cx: &mut Context<Self>,
844 ) {
845 self.editor.update(cx, |e, cx| {
846 e.activate_match(index, matches, token, window, cx)
847 })
848 }
849
850 fn select_matches(
851 &mut self,
852 matches: &[Self::Match],
853 token: SearchToken,
854 window: &mut Window,
855 cx: &mut Context<Self>,
856 ) {
857 self.editor
858 .update(cx, |e, cx| e.select_matches(matches, token, window, cx))
859 }
860
861 fn find_matches(
862 &mut self,
863 query: Arc<project::search::SearchQuery>,
864 window: &mut Window,
865 cx: &mut Context<Self>,
866 ) -> gpui::Task<Vec<Self::Match>> {
867 self.editor
868 .update(cx, |e, cx| e.find_matches(query, window, cx))
869 }
870
871 fn replace(
872 &mut self,
873 _: &Self::Match,
874 _: &SearchQuery,
875 _token: SearchToken,
876 _window: &mut Window,
877 _: &mut Context<Self>,
878 ) {
879 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
880 }
881 fn supported_options(&self) -> workspace::searchable::SearchOptions {
882 workspace::searchable::SearchOptions {
883 case: true,
884 word: true,
885 regex: true,
886 find_in_results: false,
887 // LSP log is read-only.
888 replacement: false,
889 selection: false,
890 select_all: true,
891 }
892 }
893 fn active_match_index(
894 &mut self,
895 direction: Direction,
896 matches: &[Self::Match],
897 token: SearchToken,
898 window: &mut Window,
899 cx: &mut Context<Self>,
900 ) -> Option<usize> {
901 self.editor.update(cx, |e, cx| {
902 e.active_match_index(direction, matches, token, window, cx)
903 })
904 }
905}
906
907impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
908
909impl ToolbarItemView for LspLogToolbarItemView {
910 fn set_active_pane_item(
911 &mut self,
912 active_pane_item: Option<&dyn ItemHandle>,
913 _: &mut Window,
914 cx: &mut Context<Self>,
915 ) -> workspace::ToolbarItemLocation {
916 if let Some(item) = active_pane_item
917 && let Some(log_view) = item.downcast::<LspLogView>()
918 {
919 self.log_view = Some(log_view.clone());
920 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
921 cx.notify();
922 }));
923 return ToolbarItemLocation::PrimaryLeft;
924 }
925 self.log_view = None;
926 self._log_view_subscription = None;
927 ToolbarItemLocation::Hidden
928 }
929}
930
931impl Render for LspLogToolbarItemView {
932 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
933 let Some(log_view) = self.log_view.clone() else {
934 return div();
935 };
936
937 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
938 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
939 let current_server_id = log_view.current_server_id;
940 (menu_rows, current_server_id)
941 });
942
943 let current_server = current_server_id.and_then(|current_server_id| {
944 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
945 Some(menu_rows[ix].clone())
946 } else {
947 None
948 }
949 });
950
951 let available_language_servers: Vec<_> = menu_rows
952 .into_iter()
953 .map(|row| {
954 (
955 row.server_id,
956 row.server_name,
957 row.worktree_root_name,
958 row.selected_entry,
959 )
960 })
961 .collect();
962
963 let log_toolbar_view = cx.weak_entity();
964
965 let lsp_menu = PopoverMenu::new("LspLogView")
966 .anchor(Anchor::TopLeft)
967 .trigger(
968 Button::new(
969 "language_server_menu_header",
970 current_server
971 .as_ref()
972 .map(|row| {
973 Cow::Owned(format!(
974 "{} ({})",
975 row.server_name.0, row.worktree_root_name,
976 ))
977 })
978 .unwrap_or_else(|| "No server selected".into()),
979 )
980 .end_icon(
981 Icon::new(IconName::ChevronDown)
982 .size(IconSize::Small)
983 .color(Color::Muted),
984 ),
985 )
986 .menu({
987 let log_view = log_view.clone();
988 move |window, cx| {
989 let log_view = log_view.clone();
990 ContextMenu::build(window, cx, |mut menu, window, _| {
991 for (server_id, name, worktree_root, active_entry_kind) in
992 available_language_servers.iter()
993 {
994 let label = format!("{name} ({worktree_root})");
995 let server_id = *server_id;
996 let active_entry_kind = *active_entry_kind;
997 menu = menu.entry(
998 label,
999 None,
1000 window.handler_for(&log_view, move |view, window, cx| {
1001 view.current_server_id = Some(server_id);
1002 view.active_entry_kind = active_entry_kind;
1003 match view.active_entry_kind {
1004 LogKind::Rpc => {
1005 view.toggle_rpc_trace_for_server(
1006 server_id, true, window, cx,
1007 );
1008 view.show_rpc_trace_for_server(server_id, window, cx);
1009 }
1010 LogKind::Trace => {
1011 view.show_trace_for_server(server_id, window, cx)
1012 }
1013 LogKind::Logs => {
1014 view.show_logs_for_server(server_id, window, cx)
1015 }
1016 LogKind::ServerInfo => {
1017 view.show_server_info(server_id, window, cx)
1018 }
1019 }
1020 cx.notify();
1021 }),
1022 );
1023 }
1024 menu
1025 })
1026 .into()
1027 }
1028 });
1029
1030 let view_selector = current_server.map(|server| {
1031 let server_id = server.server_id;
1032 let rpc_trace_enabled = server.rpc_trace_enabled;
1033 let log_view = log_view.clone();
1034 let label = match server.selected_entry {
1035 LogKind::Rpc => RPC_MESSAGES,
1036 LogKind::Trace => SERVER_TRACE,
1037 LogKind::Logs => SERVER_LOGS,
1038 LogKind::ServerInfo => SERVER_INFO,
1039 };
1040 PopoverMenu::new("LspViewSelector")
1041 .anchor(Anchor::TopLeft)
1042 .trigger(
1043 Button::new("language_server_menu_header", label).end_icon(
1044 Icon::new(IconName::ChevronDown)
1045 .size(IconSize::Small)
1046 .color(Color::Muted),
1047 ),
1048 )
1049 .menu(move |window, cx| {
1050 let log_toolbar_view = log_toolbar_view.upgrade()?;
1051 let log_view = log_view.clone();
1052 Some(ContextMenu::build(window, cx, move |this, window, _| {
1053 this.entry(
1054 SERVER_LOGS,
1055 None,
1056 window.handler_for(&log_view, move |view, window, cx| {
1057 view.show_logs_for_server(server_id, window, cx);
1058 }),
1059 )
1060 .entry(
1061 SERVER_TRACE,
1062 None,
1063 window.handler_for(&log_view, move |view, window, cx| {
1064 view.show_trace_for_server(server_id, window, cx);
1065 }),
1066 )
1067 .custom_entry(
1068 {
1069 let log_toolbar_view = log_toolbar_view.clone();
1070 move |window, _| {
1071 h_flex()
1072 .w_full()
1073 .justify_between()
1074 .child(Label::new(RPC_MESSAGES))
1075 .child(
1076 div().child(
1077 Checkbox::new(
1078 "LspLogEnableRpcTrace",
1079 if rpc_trace_enabled {
1080 ToggleState::Selected
1081 } else {
1082 ToggleState::Unselected
1083 },
1084 )
1085 .on_click(window.listener_for(
1086 &log_toolbar_view,
1087 move |view, selection, window, cx| {
1088 let enabled = matches!(
1089 selection,
1090 ToggleState::Selected
1091 );
1092 view.toggle_rpc_logging_for_server(
1093 server_id, enabled, window, cx,
1094 );
1095 cx.stop_propagation();
1096 },
1097 )),
1098 ),
1099 )
1100 .into_any_element()
1101 }
1102 },
1103 window.handler_for(&log_view, move |view, window, cx| {
1104 view.show_rpc_trace_for_server(server_id, window, cx);
1105 }),
1106 )
1107 .entry(
1108 SERVER_INFO,
1109 None,
1110 window.handler_for(&log_view, move |view, window, cx| {
1111 view.show_server_info(server_id, window, cx);
1112 }),
1113 )
1114 }))
1115 })
1116 });
1117
1118 h_flex()
1119 .size_full()
1120 .gap_1()
1121 .justify_between()
1122 .child(
1123 h_flex()
1124 .gap_0p5()
1125 .child(lsp_menu)
1126 .children(view_selector)
1127 .child(
1128 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1129 LogKind::Trace => {
1130 let log_view = log_view.clone();
1131 div().child(
1132 PopoverMenu::new("lsp-trace-level-menu")
1133 .anchor(Anchor::TopLeft)
1134 .trigger(
1135 Button::new(
1136 "language_server_trace_level_selector",
1137 "Trace level",
1138 )
1139 .end_icon(
1140 Icon::new(IconName::ChevronDown)
1141 .size(IconSize::Small)
1142 .color(Color::Muted),
1143 ),
1144 )
1145 .menu({
1146 let log_view = log_view;
1147
1148 move |window, cx| {
1149 let id = log_view.read(cx).current_server_id?;
1150
1151 let trace_level =
1152 log_view.update(cx, |this, cx| {
1153 this.log_store.update(cx, |this, _| {
1154 Some(
1155 this.get_language_server_state(id)?
1156 .trace_level,
1157 )
1158 })
1159 })?;
1160
1161 ContextMenu::build(
1162 window,
1163 cx,
1164 |mut menu, window, cx| {
1165 let log_view = log_view.clone();
1166
1167 for (option, label) in [
1168 (TraceValue::Off, "Off"),
1169 (TraceValue::Messages, "Messages"),
1170 (TraceValue::Verbose, "Verbose"),
1171 ] {
1172 menu = menu.entry(label, None, {
1173 let log_view = log_view.clone();
1174 move |_, cx| {
1175 log_view.update(cx, |this, cx| {
1176 if let Some(id) =
1177 this.current_server_id
1178 {
1179 this.update_trace_level(
1180 id, option, cx,
1181 );
1182 }
1183 });
1184 }
1185 });
1186 if option == trace_level {
1187 menu.select_last(window, cx);
1188 }
1189 }
1190
1191 menu
1192 },
1193 )
1194 .into()
1195 }
1196 }),
1197 )
1198 }
1199 LogKind::Logs => {
1200 let log_view = log_view.clone();
1201 div().child(
1202 PopoverMenu::new("lsp-log-level-menu")
1203 .anchor(Anchor::TopLeft)
1204 .trigger(
1205 Button::new(
1206 "language_server_log_level_selector",
1207 "Log level",
1208 )
1209 .end_icon(
1210 Icon::new(IconName::ChevronDown)
1211 .size(IconSize::Small)
1212 .color(Color::Muted),
1213 ),
1214 )
1215 .menu({
1216 let log_view = log_view;
1217
1218 move |window, cx| {
1219 let id = log_view.read(cx).current_server_id?;
1220
1221 let log_level =
1222 log_view.update(cx, |this, cx| {
1223 this.log_store.update(cx, |this, _| {
1224 Some(
1225 this.get_language_server_state(id)?
1226 .log_level,
1227 )
1228 })
1229 })?;
1230
1231 ContextMenu::build(
1232 window,
1233 cx,
1234 |mut menu, window, cx| {
1235 let log_view = log_view.clone();
1236
1237 for (option, label) in [
1238 (MessageType::LOG, "Log"),
1239 (MessageType::INFO, "Info"),
1240 (MessageType::WARNING, "Warning"),
1241 (MessageType::ERROR, "Error"),
1242 ] {
1243 menu = menu.entry(label, None, {
1244 let log_view = log_view.clone();
1245 move |window, cx| {
1246 log_view.update(cx, |this, cx| {
1247 if let Some(id) =
1248 this.current_server_id
1249 {
1250 this.update_log_level(
1251 id, option, window, cx,
1252 );
1253 }
1254 });
1255 }
1256 });
1257 if option == log_level {
1258 menu.select_last(window, cx);
1259 }
1260 }
1261
1262 menu
1263 },
1264 )
1265 .into()
1266 }
1267 }),
1268 )
1269 }
1270 _ => div(),
1271 }),
1272 ),
1273 )
1274 .child(
1275 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1276 |this, _, window, cx| {
1277 if let Some(log_view) = this.log_view.as_ref() {
1278 log_view.update(cx, |log_view, cx| {
1279 log_view.editor.update(cx, |editor, cx| {
1280 editor.set_read_only(false);
1281 editor.clear(window, cx);
1282 editor.set_read_only(true);
1283 });
1284 })
1285 }
1286 },
1287 )),
1288 )
1289 }
1290}
1291
1292fn initialize_new_editor(
1293 content: String,
1294 move_to_end: bool,
1295 window: &mut Window,
1296 cx: &mut App,
1297) -> Entity<Editor> {
1298 cx.new(|cx| {
1299 let mut editor = Editor::multi_line(window, cx);
1300 editor.hide_minimap_by_default(window, cx);
1301 editor.set_text(content, window, cx);
1302 editor.set_show_git_diff_gutter(false, cx);
1303 editor.set_show_runnables(false, cx);
1304 editor.set_show_bookmarks(false, cx);
1305 editor.set_show_breakpoints(false, cx);
1306 editor.set_read_only(true);
1307 editor.set_show_edit_predictions(Some(false), window, cx);
1308 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1309 if move_to_end {
1310 editor.move_to_end(&MoveToEnd, window, cx);
1311 }
1312 editor
1313 })
1314}
1315
1316const RPC_MESSAGES: &str = "RPC Messages";
1317const SERVER_LOGS: &str = "Server Logs";
1318const SERVER_TRACE: &str = "Server Trace";
1319const SERVER_INFO: &str = "Server Info";
1320
1321impl LspLogToolbarItemView {
1322 pub fn new() -> Self {
1323 Self {
1324 log_view: None,
1325 _log_view_subscription: None,
1326 }
1327 }
1328
1329 fn toggle_rpc_logging_for_server(
1330 &mut self,
1331 id: LanguageServerId,
1332 enabled: bool,
1333 window: &mut Window,
1334 cx: &mut Context<Self>,
1335 ) {
1336 if let Some(log_view) = &self.log_view {
1337 log_view.update(cx, |log_view, cx| {
1338 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1339 if !enabled && Some(id) == log_view.current_server_id {
1340 log_view.show_logs_for_server(id, window, cx);
1341 cx.notify();
1342 } else if enabled {
1343 log_view.show_rpc_trace_for_server(id, window, cx);
1344 cx.notify();
1345 }
1346 window.focus(&log_view.focus_handle, cx);
1347 });
1348 }
1349 cx.notify();
1350 }
1351}
1352
1353struct ServerInfo {
1354 id: LanguageServerId,
1355 capabilities: lsp::ServerCapabilities,
1356 status: LanguageServerStatus,
1357}
1358
1359impl ServerInfo {
1360 fn new(server: &LanguageServer) -> Self {
1361 Self {
1362 id: server.server_id(),
1363 capabilities: server.capabilities(),
1364 status: LanguageServerStatus {
1365 name: server.name(),
1366 language_name: None,
1367 server_version: server.version(),
1368 server_readable_version: server.readable_version(),
1369 pending_work: Default::default(),
1370 has_pending_diagnostic_updates: false,
1371 progress_tokens: Default::default(),
1372 worktree: None,
1373 binary: Some(server.binary().clone()),
1374 configuration: Some(server.configuration().clone()),
1375 workspace_folders: server.workspace_folders(),
1376 process_id: server.process_id(),
1377 },
1378 }
1379 }
1380}
1381
1382impl EventEmitter<EditorEvent> for LspLogView {}
1383impl EventEmitter<SearchEvent> for LspLogView {}
1384