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