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