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