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