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