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