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