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.binary.as_ref().map_or_else(
325 || "Unknown".to_string(),
326 |bin| bin.path.as_path().to_string_lossy().to_string()
327 ),
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 PopoverMenu::new("LspViewSelector")
942 .anchor(Corner::TopLeft)
943 .trigger(
944 Button::new("language_server_menu_header", server.selected_entry.label())
945 .icon(IconName::ChevronDown)
946 .icon_size(IconSize::Small)
947 .icon_color(Color::Muted),
948 )
949 .menu(move |window, cx| {
950 let log_toolbar_view = log_toolbar_view.clone();
951 let log_view = log_view.clone();
952 Some(ContextMenu::build(window, cx, move |this, window, _| {
953 this.entry(
954 SERVER_LOGS,
955 None,
956 window.handler_for(&log_view, move |view, window, cx| {
957 view.show_logs_for_server(server_id, window, cx);
958 }),
959 )
960 .entry(
961 SERVER_TRACE,
962 None,
963 window.handler_for(&log_view, move |view, window, cx| {
964 view.show_trace_for_server(server_id, window, cx);
965 }),
966 )
967 .custom_entry(
968 {
969 let log_toolbar_view = log_toolbar_view.clone();
970 move |window, _| {
971 h_flex()
972 .w_full()
973 .justify_between()
974 .child(Label::new(RPC_MESSAGES))
975 .child(
976 div().child(
977 Checkbox::new(
978 "LspLogEnableRpcTrace",
979 if rpc_trace_enabled {
980 ToggleState::Selected
981 } else {
982 ToggleState::Unselected
983 },
984 )
985 .on_click(window.listener_for(
986 &log_toolbar_view,
987 move |view, selection, window, cx| {
988 let enabled = matches!(
989 selection,
990 ToggleState::Selected
991 );
992 view.toggle_rpc_logging_for_server(
993 server_id, enabled, window, cx,
994 );
995 cx.stop_propagation();
996 },
997 )),
998 ),
999 )
1000 .into_any_element()
1001 }
1002 },
1003 window.handler_for(&log_view, move |view, window, cx| {
1004 view.show_rpc_trace_for_server(server_id, window, cx);
1005 }),
1006 )
1007 .entry(
1008 SERVER_INFO,
1009 None,
1010 window.handler_for(&log_view, move |view, window, cx| {
1011 view.show_server_info(server_id, window, cx);
1012 }),
1013 )
1014 }))
1015 })
1016 });
1017
1018 h_flex()
1019 .size_full()
1020 .gap_1()
1021 .justify_between()
1022 .child(
1023 h_flex()
1024 .gap_0p5()
1025 .child(lsp_menu)
1026 .children(view_selector)
1027 .child(
1028 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1029 LogKind::Trace => {
1030 let log_view = log_view.clone();
1031 div().child(
1032 PopoverMenu::new("lsp-trace-level-menu")
1033 .anchor(Corner::TopLeft)
1034 .trigger(
1035 Button::new(
1036 "language_server_trace_level_selector",
1037 "Trace level",
1038 )
1039 .icon(IconName::ChevronDown)
1040 .icon_size(IconSize::Small)
1041 .icon_color(Color::Muted),
1042 )
1043 .menu({
1044 let log_view = log_view;
1045
1046 move |window, cx| {
1047 let id = log_view.read(cx).current_server_id?;
1048
1049 let trace_level =
1050 log_view.update(cx, |this, cx| {
1051 this.log_store.update(cx, |this, _| {
1052 Some(
1053 this.get_language_server_state(id)?
1054 .trace_level,
1055 )
1056 })
1057 })?;
1058
1059 ContextMenu::build(
1060 window,
1061 cx,
1062 |mut menu, window, cx| {
1063 let log_view = log_view.clone();
1064
1065 for (option, label) in [
1066 (TraceValue::Off, "Off"),
1067 (TraceValue::Messages, "Messages"),
1068 (TraceValue::Verbose, "Verbose"),
1069 ] {
1070 menu = menu.entry(label, None, {
1071 let log_view = log_view.clone();
1072 move |_, cx| {
1073 log_view.update(cx, |this, cx| {
1074 if let Some(id) =
1075 this.current_server_id
1076 {
1077 this.update_trace_level(
1078 id, option, cx,
1079 );
1080 }
1081 });
1082 }
1083 });
1084 if option == trace_level {
1085 menu.select_last(window, cx);
1086 }
1087 }
1088
1089 menu
1090 },
1091 )
1092 .into()
1093 }
1094 }),
1095 )
1096 }
1097 LogKind::Logs => {
1098 let log_view = log_view.clone();
1099 div().child(
1100 PopoverMenu::new("lsp-log-level-menu")
1101 .anchor(Corner::TopLeft)
1102 .trigger(
1103 Button::new(
1104 "language_server_log_level_selector",
1105 "Log level",
1106 )
1107 .icon(IconName::ChevronDown)
1108 .icon_size(IconSize::Small)
1109 .icon_color(Color::Muted),
1110 )
1111 .menu({
1112 let log_view = log_view;
1113
1114 move |window, cx| {
1115 let id = log_view.read(cx).current_server_id?;
1116
1117 let log_level =
1118 log_view.update(cx, |this, cx| {
1119 this.log_store.update(cx, |this, _| {
1120 Some(
1121 this.get_language_server_state(id)?
1122 .log_level,
1123 )
1124 })
1125 })?;
1126
1127 ContextMenu::build(
1128 window,
1129 cx,
1130 |mut menu, window, cx| {
1131 let log_view = log_view.clone();
1132
1133 for (option, label) in [
1134 (MessageType::LOG, "Log"),
1135 (MessageType::INFO, "Info"),
1136 (MessageType::WARNING, "Warning"),
1137 (MessageType::ERROR, "Error"),
1138 ] {
1139 menu = menu.entry(label, None, {
1140 let log_view = log_view.clone();
1141 move |window, cx| {
1142 log_view.update(cx, |this, cx| {
1143 if let Some(id) =
1144 this.current_server_id
1145 {
1146 this.update_log_level(
1147 id, option, window, cx,
1148 );
1149 }
1150 });
1151 }
1152 });
1153 if option == log_level {
1154 menu.select_last(window, cx);
1155 }
1156 }
1157
1158 menu
1159 },
1160 )
1161 .into()
1162 }
1163 }),
1164 )
1165 }
1166 _ => div(),
1167 }),
1168 ),
1169 )
1170 .child(
1171 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1172 |this, _, window, cx| {
1173 if let Some(log_view) = this.log_view.as_ref() {
1174 log_view.update(cx, |log_view, cx| {
1175 log_view.editor.update(cx, |editor, cx| {
1176 editor.set_read_only(false);
1177 editor.clear(window, cx);
1178 editor.set_read_only(true);
1179 });
1180 })
1181 }
1182 },
1183 )),
1184 )
1185 }
1186}
1187
1188fn initialize_new_editor(
1189 content: String,
1190 move_to_end: bool,
1191 window: &mut Window,
1192 cx: &mut App,
1193) -> Entity<Editor> {
1194 cx.new(|cx| {
1195 let mut editor = Editor::multi_line(window, cx);
1196 editor.hide_minimap_by_default(window, cx);
1197 editor.set_text(content, window, cx);
1198 editor.set_show_git_diff_gutter(false, cx);
1199 editor.set_show_runnables(false, cx);
1200 editor.set_show_breakpoints(false, cx);
1201 editor.set_read_only(true);
1202 editor.set_show_edit_predictions(Some(false), window, cx);
1203 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1204 if move_to_end {
1205 editor.move_to_end(&MoveToEnd, window, cx);
1206 }
1207 editor
1208 })
1209}
1210
1211const RPC_MESSAGES: &str = "RPC Messages";
1212const SERVER_LOGS: &str = "Server Logs";
1213const SERVER_TRACE: &str = "Server Trace";
1214const SERVER_INFO: &str = "Server Info";
1215
1216impl LspLogToolbarItemView {
1217 pub fn new() -> Self {
1218 Self {
1219 log_view: None,
1220 _log_view_subscription: None,
1221 }
1222 }
1223
1224 fn toggle_rpc_logging_for_server(
1225 &mut self,
1226 id: LanguageServerId,
1227 enabled: bool,
1228 window: &mut Window,
1229 cx: &mut Context<Self>,
1230 ) {
1231 if let Some(log_view) = &self.log_view {
1232 log_view.update(cx, |log_view, cx| {
1233 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1234 if !enabled && Some(id) == log_view.current_server_id {
1235 log_view.show_logs_for_server(id, window, cx);
1236 cx.notify();
1237 } else if enabled {
1238 log_view.show_rpc_trace_for_server(id, window, cx);
1239 cx.notify();
1240 }
1241 window.focus(&log_view.focus_handle);
1242 });
1243 }
1244 cx.notify();
1245 }
1246}
1247
1248struct ServerInfo {
1249 id: LanguageServerId,
1250 capabilities: lsp::ServerCapabilities,
1251 binary: Option<LanguageServerBinary>,
1252 name: LanguageServerName,
1253 workspace_folders: Vec<String>,
1254 configuration: Option<serde_json::Value>,
1255}
1256
1257impl ServerInfo {
1258 fn new(server: &LanguageServer) -> Self {
1259 Self {
1260 id: server.server_id(),
1261 capabilities: server.capabilities(),
1262 binary: Some(server.binary().clone()),
1263 name: server.name(),
1264 workspace_folders: server
1265 .workspace_folders()
1266 .into_iter()
1267 .filter_map(|path| {
1268 path.to_file_path()
1269 .ok()
1270 .map(|path| path.to_string_lossy().into_owned())
1271 })
1272 .collect::<Vec<_>>(),
1273 configuration: Some(server.configuration().clone()),
1274 }
1275 }
1276}
1277
1278impl EventEmitter<EditorEvent> for LspLogView {}
1279impl EventEmitter<SearchEvent> for LspLogView {}