1use collections::{HashMap, VecDeque};
2use copilot::Copilot;
3use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
4use futures::{StreamExt, channel::mpsc};
5use gpui::{
6 AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, Global,
7 IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
8};
9use itertools::Itertools;
10use language::{LanguageServerId, language_settings::SoftWrap};
11use lsp::{
12 IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
13 SetTraceParams, TraceValue, notification::SetTrace,
14};
15use project::{Project, WorktreeId, search::SearchQuery};
16use std::{any::TypeId, borrow::Cow, sync::Arc};
17use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
18use workspace::{
19 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
20 item::{Item, ItemHandle},
21 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
22};
23
24use crate::get_or_create_tool;
25
26const SEND_LINE: &str = "\n// Send:";
27const RECEIVE_LINE: &str = "\n// Receive:";
28const MAX_STORED_LOG_ENTRIES: usize = 2000;
29
30pub struct LogStore {
31 projects: HashMap<WeakEntity<Project>, ProjectState>,
32 language_servers: HashMap<LanguageServerId, LanguageServerState>,
33 copilot_log_subscription: Option<lsp::Subscription>,
34 _copilot_subscription: Option<gpui::Subscription>,
35 io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
36}
37
38struct ProjectState {
39 _subscriptions: [gpui::Subscription; 2],
40}
41
42trait Message: AsRef<str> {
43 type Level: Copy + std::fmt::Debug;
44 fn should_include(&self, _: Self::Level) -> bool {
45 true
46 }
47}
48
49pub(super) struct LogMessage {
50 message: String,
51 typ: MessageType,
52}
53
54impl AsRef<str> for LogMessage {
55 fn as_ref(&self) -> &str {
56 &self.message
57 }
58}
59
60impl Message for LogMessage {
61 type Level = MessageType;
62
63 fn should_include(&self, level: Self::Level) -> bool {
64 match (self.typ, level) {
65 (MessageType::ERROR, _) => true,
66 (_, MessageType::ERROR) => false,
67 (MessageType::WARNING, _) => true,
68 (_, MessageType::WARNING) => false,
69 (MessageType::INFO, _) => true,
70 (_, MessageType::INFO) => false,
71 _ => true,
72 }
73 }
74}
75
76pub(super) struct TraceMessage {
77 message: String,
78}
79
80impl AsRef<str> for TraceMessage {
81 fn as_ref(&self) -> &str {
82 &self.message
83 }
84}
85
86impl Message for TraceMessage {
87 type Level = ();
88}
89
90struct RpcMessage {
91 message: String,
92}
93
94impl AsRef<str> for RpcMessage {
95 fn as_ref(&self) -> &str {
96 &self.message
97 }
98}
99
100impl Message for RpcMessage {
101 type Level = ();
102}
103
104pub(super) struct LanguageServerState {
105 name: Option<LanguageServerName>,
106 worktree_id: Option<WorktreeId>,
107 kind: LanguageServerKind,
108 log_messages: VecDeque<LogMessage>,
109 trace_messages: VecDeque<TraceMessage>,
110 rpc_state: Option<LanguageServerRpcState>,
111 trace_level: TraceValue,
112 log_level: MessageType,
113 io_logs_subscription: Option<lsp::Subscription>,
114}
115
116#[derive(PartialEq, Clone)]
117pub enum LanguageServerKind {
118 Local { project: WeakEntity<Project> },
119 Remote { project: WeakEntity<Project> },
120 Global,
121}
122
123impl LanguageServerKind {
124 fn is_remote(&self) -> bool {
125 matches!(self, LanguageServerKind::Remote { .. })
126 }
127}
128
129impl std::fmt::Debug for LanguageServerKind {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
133 LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
134 LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
135 }
136 }
137}
138
139impl LanguageServerKind {
140 fn project(&self) -> Option<&WeakEntity<Project>> {
141 match self {
142 Self::Local { project } => Some(project),
143 Self::Remote { project } => Some(project),
144 Self::Global { .. } => None,
145 }
146 }
147}
148
149struct LanguageServerRpcState {
150 rpc_messages: VecDeque<RpcMessage>,
151 last_message_kind: Option<MessageKind>,
152}
153
154pub struct LspLogView {
155 pub(crate) editor: Entity<Editor>,
156 editor_subscriptions: Vec<Subscription>,
157 log_store: Entity<LogStore>,
158 current_server_id: Option<LanguageServerId>,
159 active_entry_kind: LogKind,
160 project: Entity<Project>,
161 focus_handle: FocusHandle,
162 _log_store_subscriptions: Vec<Subscription>,
163}
164
165pub struct LspLogToolbarItemView {
166 log_view: Option<Entity<LspLogView>>,
167 _log_view_subscription: Option<Subscription>,
168}
169
170#[derive(Copy, Clone, PartialEq, Eq)]
171enum MessageKind {
172 Send,
173 Receive,
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq)]
177pub enum LogKind {
178 Rpc,
179 Trace,
180 #[default]
181 Logs,
182 ServerInfo,
183}
184
185impl LogKind {
186 fn label(&self) -> &'static str {
187 match self {
188 LogKind::Rpc => RPC_MESSAGES,
189 LogKind::Trace => SERVER_TRACE,
190 LogKind::Logs => SERVER_LOGS,
191 LogKind::ServerInfo => SERVER_INFO,
192 }
193 }
194}
195
196#[derive(Clone, Debug, PartialEq)]
197pub(crate) struct LogMenuItem {
198 pub server_id: LanguageServerId,
199 pub server_name: LanguageServerName,
200 pub worktree_root_name: String,
201 pub rpc_trace_enabled: bool,
202 pub selected_entry: LogKind,
203 pub trace_level: lsp::TraceValue,
204 pub server_kind: LanguageServerKind,
205}
206
207actions!(
208 dev,
209 [
210 /// Opens the language server protocol logs viewer.
211 OpenLanguageServerLogs
212 ]
213);
214
215pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
216
217impl Global for GlobalLogStore {}
218
219pub fn init(cx: &mut App) {
220 let log_store = cx.new(LogStore::new);
221 cx.set_global(GlobalLogStore(log_store.downgrade()));
222
223 cx.observe_new(move |workspace: &mut Workspace, _, cx| {
224 let project = workspace.project();
225 if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
226 log_store.update(cx, |store, cx| {
227 store.add_project(project, cx);
228 });
229 }
230
231 let log_store = log_store.clone();
232 workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
233 let project = workspace.project().read(cx);
234 if project.is_local() || project.is_via_ssh() {
235 let project = workspace.project().clone();
236 let log_store = log_store.clone();
237 get_or_create_tool(
238 workspace,
239 SplitDirection::Right,
240 window,
241 cx,
242 move |window, cx| LspLogView::new(project, log_store, window, cx),
243 );
244 }
245 });
246 })
247 .detach();
248}
249
250impl LogStore {
251 pub fn new(cx: &mut Context<Self>) -> Self {
252 let (io_tx, mut io_rx) = mpsc::unbounded();
253
254 let copilot_subscription = Copilot::global(cx).map(|copilot| {
255 let copilot = &copilot;
256 cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| {
257 if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event
258 && let Some(server) = copilot.read(cx).language_server()
259 {
260 let server_id = server.server_id();
261 let weak_this = cx.weak_entity();
262 this.copilot_log_subscription =
263 Some(server.on_notification::<copilot::request::LogMessage, _>(
264 move |params, cx| {
265 weak_this
266 .update(cx, |this, cx| {
267 this.add_language_server_log(
268 server_id,
269 MessageType::LOG,
270 ¶ms.message,
271 cx,
272 );
273 })
274 .ok();
275 },
276 ));
277 let name = LanguageServerName::new_static("copilot");
278 this.add_language_server(
279 LanguageServerKind::Global,
280 server.server_id(),
281 Some(name),
282 None,
283 Some(server.clone()),
284 cx,
285 );
286 }
287 })
288 });
289
290 let this = Self {
291 copilot_log_subscription: None,
292 _copilot_subscription: copilot_subscription,
293 projects: HashMap::default(),
294 language_servers: HashMap::default(),
295 io_tx,
296 };
297
298 cx.spawn(async move |this, cx| {
299 while let Some((server_id, io_kind, message)) = io_rx.next().await {
300 if let Some(this) = this.upgrade() {
301 this.update(cx, |this, cx| {
302 this.on_io(server_id, io_kind, &message, cx);
303 })?;
304 }
305 }
306 anyhow::Ok(())
307 })
308 .detach_and_log_err(cx);
309 this
310 }
311
312 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
313 let weak_project = project.downgrade();
314 self.projects.insert(
315 project.downgrade(),
316 ProjectState {
317 _subscriptions: [
318 cx.observe_release(project, move |this, _, _| {
319 this.projects.remove(&weak_project);
320 this.language_servers
321 .retain(|_, state| state.kind.project() != Some(&weak_project));
322 }),
323 cx.subscribe(project, |this, project, event, cx| {
324 let server_kind = if project.read(cx).is_via_ssh() {
325 LanguageServerKind::Remote {
326 project: project.downgrade(),
327 }
328 } else {
329 LanguageServerKind::Local {
330 project: project.downgrade(),
331 }
332 };
333
334 match event {
335 project::Event::LanguageServerAdded(id, name, worktree_id) => {
336 this.add_language_server(
337 server_kind,
338 *id,
339 Some(name.clone()),
340 *worktree_id,
341 project
342 .read(cx)
343 .lsp_store()
344 .read(cx)
345 .language_server_for_id(*id),
346 cx,
347 );
348 }
349 project::Event::LanguageServerRemoved(id) => {
350 this.remove_language_server(*id, cx);
351 }
352 project::Event::LanguageServerLog(id, typ, message) => {
353 this.add_language_server(server_kind, *id, None, None, None, cx);
354 match typ {
355 project::LanguageServerLogType::Log(typ) => {
356 this.add_language_server_log(*id, *typ, message, cx);
357 }
358 project::LanguageServerLogType::Trace(_) => {
359 this.add_language_server_trace(*id, message, cx);
360 }
361 }
362 }
363 _ => {}
364 }
365 }),
366 ],
367 },
368 );
369 }
370
371 pub(super) fn get_language_server_state(
372 &mut self,
373 id: LanguageServerId,
374 ) -> Option<&mut LanguageServerState> {
375 self.language_servers.get_mut(&id)
376 }
377
378 fn add_language_server(
379 &mut self,
380 kind: LanguageServerKind,
381 server_id: LanguageServerId,
382 name: Option<LanguageServerName>,
383 worktree_id: Option<WorktreeId>,
384 server: Option<Arc<LanguageServer>>,
385 cx: &mut Context<Self>,
386 ) -> Option<&mut LanguageServerState> {
387 let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
388 cx.notify();
389 LanguageServerState {
390 name: None,
391 worktree_id: None,
392 kind,
393 rpc_state: None,
394 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
395 trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
396 trace_level: TraceValue::Off,
397 log_level: MessageType::LOG,
398 io_logs_subscription: None,
399 }
400 });
401
402 if let Some(name) = name {
403 server_state.name = Some(name);
404 }
405 if let Some(worktree_id) = worktree_id {
406 server_state.worktree_id = Some(worktree_id);
407 }
408
409 if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
410 let io_tx = self.io_tx.clone();
411 let server_id = server.server_id();
412 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
413 io_tx
414 .unbounded_send((server_id, io_kind, message.to_string()))
415 .ok();
416 }));
417 }
418
419 Some(server_state)
420 }
421
422 fn add_language_server_log(
423 &mut self,
424 id: LanguageServerId,
425 typ: MessageType,
426 message: &str,
427 cx: &mut Context<Self>,
428 ) -> Option<()> {
429 let language_server_state = self.get_language_server_state(id)?;
430
431 let log_lines = &mut language_server_state.log_messages;
432 Self::add_language_server_message(
433 log_lines,
434 id,
435 LogMessage {
436 message: message.trim_end().to_string(),
437 typ,
438 },
439 language_server_state.log_level,
440 LogKind::Logs,
441 cx,
442 );
443 Some(())
444 }
445
446 fn add_language_server_trace(
447 &mut self,
448 id: LanguageServerId,
449 message: &str,
450 cx: &mut Context<Self>,
451 ) -> Option<()> {
452 let language_server_state = self.get_language_server_state(id)?;
453
454 let log_lines = &mut language_server_state.trace_messages;
455 Self::add_language_server_message(
456 log_lines,
457 id,
458 TraceMessage {
459 message: message.trim().to_string(),
460 },
461 (),
462 LogKind::Trace,
463 cx,
464 );
465 Some(())
466 }
467
468 fn add_language_server_message<T: Message>(
469 log_lines: &mut VecDeque<T>,
470 id: LanguageServerId,
471 message: T,
472 current_severity: <T as Message>::Level,
473 kind: LogKind,
474 cx: &mut Context<Self>,
475 ) {
476 while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
477 log_lines.pop_front();
478 }
479 let text = message.as_ref().to_string();
480 let visible = message.should_include(current_severity);
481 log_lines.push_back(message);
482
483 if visible {
484 cx.emit(Event::NewServerLogEntry { id, kind, text });
485 cx.notify();
486 }
487 }
488
489 fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
490 self.language_servers.remove(&id);
491 cx.notify();
492 }
493
494 pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
495 Some(&self.language_servers.get(&server_id)?.log_messages)
496 }
497
498 pub(super) fn server_trace(
499 &self,
500 server_id: LanguageServerId,
501 ) -> Option<&VecDeque<TraceMessage>> {
502 Some(&self.language_servers.get(&server_id)?.trace_messages)
503 }
504
505 fn server_ids_for_project<'a>(
506 &'a self,
507 lookup_project: &'a WeakEntity<Project>,
508 ) -> impl Iterator<Item = LanguageServerId> + 'a {
509 self.language_servers
510 .iter()
511 .filter_map(move |(id, state)| match &state.kind {
512 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
513 if project == lookup_project {
514 Some(*id)
515 } else {
516 None
517 }
518 }
519 LanguageServerKind::Global => Some(*id),
520 })
521 }
522
523 fn enable_rpc_trace_for_language_server(
524 &mut self,
525 server_id: LanguageServerId,
526 ) -> Option<&mut LanguageServerRpcState> {
527 let rpc_state = self
528 .language_servers
529 .get_mut(&server_id)?
530 .rpc_state
531 .get_or_insert_with(|| LanguageServerRpcState {
532 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
533 last_message_kind: None,
534 });
535 Some(rpc_state)
536 }
537
538 pub fn disable_rpc_trace_for_language_server(
539 &mut self,
540 server_id: LanguageServerId,
541 ) -> Option<()> {
542 self.language_servers.get_mut(&server_id)?.rpc_state.take();
543 Some(())
544 }
545
546 pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
547 match server {
548 LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
549 LanguageServerSelector::Name(name) => self
550 .language_servers
551 .iter()
552 .any(|(_, state)| state.name.as_ref() == Some(name)),
553 }
554 }
555
556 pub fn open_server_log(
557 &mut self,
558 workspace: WeakEntity<Workspace>,
559 server: LanguageServerSelector,
560 window: &mut Window,
561 cx: &mut Context<Self>,
562 ) {
563 cx.spawn_in(window, async move |log_store, cx| {
564 let Some(log_store) = log_store.upgrade() else {
565 return;
566 };
567 workspace
568 .update_in(cx, |workspace, window, cx| {
569 let project = workspace.project().clone();
570 let tool_log_store = log_store.clone();
571 let log_view = get_or_create_tool(
572 workspace,
573 SplitDirection::Right,
574 window,
575 cx,
576 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
577 );
578 log_view.update(cx, |log_view, cx| {
579 let server_id = match server {
580 LanguageServerSelector::Id(id) => Some(id),
581 LanguageServerSelector::Name(name) => {
582 log_store.read(cx).language_servers.iter().find_map(
583 |(id, state)| {
584 if state.name.as_ref() == Some(&name) {
585 Some(*id)
586 } else {
587 None
588 }
589 },
590 )
591 }
592 };
593 if let Some(server_id) = server_id {
594 log_view.show_logs_for_server(server_id, window, cx);
595 }
596 });
597 })
598 .ok();
599 })
600 .detach();
601 }
602
603 pub fn open_server_trace(
604 &mut self,
605 workspace: WeakEntity<Workspace>,
606 server: LanguageServerSelector,
607 window: &mut Window,
608 cx: &mut Context<Self>,
609 ) {
610 cx.spawn_in(window, async move |log_store, cx| {
611 let Some(log_store) = log_store.upgrade() else {
612 return;
613 };
614 workspace
615 .update_in(cx, |workspace, window, cx| {
616 let project = workspace.project().clone();
617 let tool_log_store = log_store.clone();
618 let log_view = get_or_create_tool(
619 workspace,
620 SplitDirection::Right,
621 window,
622 cx,
623 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
624 );
625 log_view.update(cx, |log_view, cx| {
626 let server_id = match server {
627 LanguageServerSelector::Id(id) => Some(id),
628 LanguageServerSelector::Name(name) => {
629 log_store.read(cx).language_servers.iter().find_map(
630 |(id, state)| {
631 if state.name.as_ref() == Some(&name) {
632 Some(*id)
633 } else {
634 None
635 }
636 },
637 )
638 }
639 };
640 if let Some(server_id) = server_id {
641 log_view.show_rpc_trace_for_server(server_id, window, cx);
642 }
643 });
644 })
645 .ok();
646 })
647 .detach();
648 }
649
650 fn on_io(
651 &mut self,
652 language_server_id: LanguageServerId,
653 io_kind: IoKind,
654 message: &str,
655 cx: &mut Context<Self>,
656 ) -> Option<()> {
657 let is_received = match io_kind {
658 IoKind::StdOut => true,
659 IoKind::StdIn => false,
660 IoKind::StdErr => {
661 self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
662 return Some(());
663 }
664 };
665
666 let state = self
667 .get_language_server_state(language_server_id)?
668 .rpc_state
669 .as_mut()?;
670 let kind = if is_received {
671 MessageKind::Receive
672 } else {
673 MessageKind::Send
674 };
675
676 let rpc_log_lines = &mut state.rpc_messages;
677 if state.last_message_kind != Some(kind) {
678 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
679 rpc_log_lines.pop_front();
680 }
681 let line_before_message = match kind {
682 MessageKind::Send => SEND_LINE,
683 MessageKind::Receive => RECEIVE_LINE,
684 };
685 rpc_log_lines.push_back(RpcMessage {
686 message: line_before_message.to_string(),
687 });
688 cx.emit(Event::NewServerLogEntry {
689 id: language_server_id,
690 kind: LogKind::Rpc,
691 text: line_before_message.to_string(),
692 });
693 }
694
695 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
696 rpc_log_lines.pop_front();
697 }
698
699 let message = message.trim();
700 rpc_log_lines.push_back(RpcMessage {
701 message: message.to_string(),
702 });
703 cx.emit(Event::NewServerLogEntry {
704 id: language_server_id,
705 kind: LogKind::Rpc,
706 text: message.to_string(),
707 });
708 cx.notify();
709 Some(())
710 }
711}
712
713impl LspLogView {
714 pub fn new(
715 project: Entity<Project>,
716 log_store: Entity<LogStore>,
717 window: &mut Window,
718 cx: &mut Context<Self>,
719 ) -> Self {
720 let server_id = log_store
721 .read(cx)
722 .language_servers
723 .iter()
724 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
725 .map(|(id, _)| *id);
726
727 let weak_project = project.downgrade();
728 let model_changes_subscription =
729 cx.observe_in(&log_store, window, move |this, store, window, cx| {
730 let first_server_id_for_project =
731 store.read(cx).server_ids_for_project(&weak_project).next();
732 if let Some(current_lsp) = this.current_server_id {
733 if !store.read(cx).language_servers.contains_key(¤t_lsp)
734 && let Some(server_id) = first_server_id_for_project
735 {
736 match this.active_entry_kind {
737 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
738 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
739 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
740 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
741 }
742 }
743 } else if let Some(server_id) = first_server_id_for_project {
744 match this.active_entry_kind {
745 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
746 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
747 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
748 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
749 }
750 }
751
752 cx.notify();
753 });
754 let events_subscriptions = cx.subscribe_in(
755 &log_store,
756 window,
757 move |log_view, _, e, window, cx| match e {
758 Event::NewServerLogEntry { id, kind, text } => {
759 if log_view.current_server_id == Some(*id)
760 && *kind == log_view.active_entry_kind
761 {
762 log_view.editor.update(cx, |editor, cx| {
763 editor.set_read_only(false);
764 let last_offset = editor.buffer().read(cx).len(cx);
765 let newest_cursor_is_at_end =
766 editor.selections.newest::<usize>(cx).start >= last_offset;
767 editor.edit(
768 vec![
769 (last_offset..last_offset, text.as_str()),
770 (last_offset..last_offset, "\n"),
771 ],
772 cx,
773 );
774 if text.len() > 1024
775 && let Some((fold_offset, _)) =
776 text.char_indices().dropping(1024).next()
777 && fold_offset < text.len()
778 {
779 editor.fold_ranges(
780 vec![last_offset + fold_offset..last_offset + text.len()],
781 false,
782 window,
783 cx,
784 );
785 }
786
787 if newest_cursor_is_at_end {
788 editor.request_autoscroll(Autoscroll::bottom(), cx);
789 }
790 editor.set_read_only(true);
791 });
792 }
793 }
794 },
795 );
796 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
797
798 let focus_handle = cx.focus_handle();
799 let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
800 window.focus(&log_view.editor.focus_handle(cx));
801 });
802
803 let mut this = Self {
804 focus_handle,
805 editor,
806 editor_subscriptions,
807 project,
808 log_store,
809 current_server_id: None,
810 active_entry_kind: LogKind::Logs,
811 _log_store_subscriptions: vec![
812 model_changes_subscription,
813 events_subscriptions,
814 focus_subscription,
815 ],
816 };
817 if let Some(server_id) = server_id {
818 this.show_logs_for_server(server_id, window, cx);
819 }
820 this
821 }
822
823 fn editor_for_logs(
824 log_contents: String,
825 window: &mut Window,
826 cx: &mut Context<Self>,
827 ) -> (Entity<Editor>, Vec<Subscription>) {
828 let editor = initialize_new_editor(log_contents, true, window, cx);
829 let editor_subscription = cx.subscribe(
830 &editor,
831 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
832 );
833 let search_subscription = cx.subscribe(
834 &editor,
835 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
836 );
837 (editor, vec![editor_subscription, search_subscription])
838 }
839
840 fn editor_for_server_info(
841 server: &LanguageServer,
842 window: &mut Window,
843 cx: &mut Context<Self>,
844 ) -> (Entity<Editor>, Vec<Subscription>) {
845 let server_info = format!(
846 "* Server: {NAME} (id {ID})
847
848* Binary: {BINARY:#?}
849
850* Registered workspace folders:
851{WORKSPACE_FOLDERS}
852
853* Capabilities: {CAPABILITIES}
854
855* Configuration: {CONFIGURATION}",
856 NAME = server.name(),
857 ID = server.server_id(),
858 BINARY = server.binary(),
859 WORKSPACE_FOLDERS = server
860 .workspace_folders()
861 .into_iter()
862 .filter_map(|path| path
863 .to_file_path()
864 .ok()
865 .map(|path| path.to_string_lossy().into_owned()))
866 .collect::<Vec<_>>()
867 .join(", "),
868 CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
869 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
870 CONFIGURATION = serde_json::to_string_pretty(server.configuration())
871 .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
872 );
873 let editor = initialize_new_editor(server_info, false, window, cx);
874 let editor_subscription = cx.subscribe(
875 &editor,
876 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
877 );
878 let search_subscription = cx.subscribe(
879 &editor,
880 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
881 );
882 (editor, vec![editor_subscription, search_subscription])
883 }
884
885 pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
886 let log_store = self.log_store.read(cx);
887
888 let unknown_server = LanguageServerName::new_static("unknown server");
889
890 let mut rows = log_store
891 .language_servers
892 .iter()
893 .map(|(server_id, state)| match &state.kind {
894 LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
895 let worktree_root_name = state
896 .worktree_id
897 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
898 .map(|worktree| worktree.read(cx).root_name().to_string())
899 .unwrap_or_else(|| "Unknown worktree".to_string());
900
901 LogMenuItem {
902 server_id: *server_id,
903 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
904 server_kind: state.kind.clone(),
905 worktree_root_name,
906 rpc_trace_enabled: state.rpc_state.is_some(),
907 selected_entry: self.active_entry_kind,
908 trace_level: lsp::TraceValue::Off,
909 }
910 }
911
912 LanguageServerKind::Global => LogMenuItem {
913 server_id: *server_id,
914 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
915 server_kind: state.kind.clone(),
916 worktree_root_name: "supplementary".to_string(),
917 rpc_trace_enabled: state.rpc_state.is_some(),
918 selected_entry: self.active_entry_kind,
919 trace_level: lsp::TraceValue::Off,
920 },
921 })
922 .chain(
923 self.project
924 .read(cx)
925 .supplementary_language_servers(cx)
926 .filter_map(|(server_id, name)| {
927 let state = log_store.language_servers.get(&server_id)?;
928 Some(LogMenuItem {
929 server_id,
930 server_name: name,
931 server_kind: state.kind.clone(),
932 worktree_root_name: "supplementary".to_string(),
933 rpc_trace_enabled: state.rpc_state.is_some(),
934 selected_entry: self.active_entry_kind,
935 trace_level: lsp::TraceValue::Off,
936 })
937 }),
938 )
939 .collect::<Vec<_>>();
940 rows.sort_by_key(|row| row.server_id);
941 rows.dedup_by_key(|row| row.server_id);
942 Some(rows)
943 }
944
945 fn show_logs_for_server(
946 &mut self,
947 server_id: LanguageServerId,
948 window: &mut Window,
949 cx: &mut Context<Self>,
950 ) {
951 let typ = self
952 .log_store
953 .read(cx)
954 .language_servers
955 .get(&server_id)
956 .map(|v| v.log_level)
957 .unwrap_or(MessageType::LOG);
958 let log_contents = self
959 .log_store
960 .read(cx)
961 .server_logs(server_id)
962 .map(|v| log_contents(v, typ));
963 if let Some(log_contents) = log_contents {
964 self.current_server_id = Some(server_id);
965 self.active_entry_kind = LogKind::Logs;
966 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
967 self.editor = editor;
968 self.editor_subscriptions = editor_subscriptions;
969 cx.notify();
970 }
971 self.editor.read(cx).focus_handle(cx).focus(window);
972 }
973
974 fn update_log_level(
975 &self,
976 server_id: LanguageServerId,
977 level: MessageType,
978 window: &mut Window,
979 cx: &mut Context<Self>,
980 ) {
981 let log_contents = self.log_store.update(cx, |this, _| {
982 if let Some(state) = this.get_language_server_state(server_id) {
983 state.log_level = level;
984 }
985
986 this.server_logs(server_id).map(|v| log_contents(v, level))
987 });
988
989 if let Some(log_contents) = log_contents {
990 self.editor.update(cx, |editor, cx| {
991 editor.set_text(log_contents, window, cx);
992 editor.move_to_end(&MoveToEnd, window, cx);
993 });
994 cx.notify();
995 }
996
997 self.editor.read(cx).focus_handle(cx).focus(window);
998 }
999
1000 fn show_trace_for_server(
1001 &mut self,
1002 server_id: LanguageServerId,
1003 window: &mut Window,
1004 cx: &mut Context<Self>,
1005 ) {
1006 let log_contents = self
1007 .log_store
1008 .read(cx)
1009 .server_trace(server_id)
1010 .map(|v| log_contents(v, ()));
1011 if let Some(log_contents) = log_contents {
1012 self.current_server_id = Some(server_id);
1013 self.active_entry_kind = LogKind::Trace;
1014 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
1015 self.editor = editor;
1016 self.editor_subscriptions = editor_subscriptions;
1017 cx.notify();
1018 }
1019 self.editor.read(cx).focus_handle(cx).focus(window);
1020 }
1021
1022 fn show_rpc_trace_for_server(
1023 &mut self,
1024 server_id: LanguageServerId,
1025 window: &mut Window,
1026 cx: &mut Context<Self>,
1027 ) {
1028 let rpc_log = self.log_store.update(cx, |log_store, _| {
1029 log_store
1030 .enable_rpc_trace_for_language_server(server_id)
1031 .map(|state| log_contents(&state.rpc_messages, ()))
1032 });
1033 if let Some(rpc_log) = rpc_log {
1034 self.current_server_id = Some(server_id);
1035 self.active_entry_kind = LogKind::Rpc;
1036 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
1037 let language = self.project.read(cx).languages().language_for_name("JSON");
1038 editor
1039 .read(cx)
1040 .buffer()
1041 .read(cx)
1042 .as_singleton()
1043 .expect("log buffer should be a singleton")
1044 .update(cx, |_, cx| {
1045 cx.spawn({
1046 let buffer = cx.entity();
1047 async move |_, cx| {
1048 let language = language.await.ok();
1049 buffer.update(cx, |buffer, cx| {
1050 buffer.set_language(language, cx);
1051 })
1052 }
1053 })
1054 .detach_and_log_err(cx);
1055 });
1056
1057 self.editor = editor;
1058 self.editor_subscriptions = editor_subscriptions;
1059 cx.notify();
1060 }
1061
1062 self.editor.read(cx).focus_handle(cx).focus(window);
1063 }
1064
1065 fn toggle_rpc_trace_for_server(
1066 &mut self,
1067 server_id: LanguageServerId,
1068 enabled: bool,
1069 window: &mut Window,
1070 cx: &mut Context<Self>,
1071 ) {
1072 self.log_store.update(cx, |log_store, _| {
1073 if enabled {
1074 log_store.enable_rpc_trace_for_language_server(server_id);
1075 } else {
1076 log_store.disable_rpc_trace_for_language_server(server_id);
1077 }
1078 });
1079 if !enabled && Some(server_id) == self.current_server_id {
1080 self.show_logs_for_server(server_id, window, cx);
1081 cx.notify();
1082 }
1083 }
1084
1085 fn update_trace_level(
1086 &self,
1087 server_id: LanguageServerId,
1088 level: TraceValue,
1089 cx: &mut Context<Self>,
1090 ) {
1091 if let Some(server) = self
1092 .project
1093 .read(cx)
1094 .lsp_store()
1095 .read(cx)
1096 .language_server_for_id(server_id)
1097 {
1098 self.log_store.update(cx, |this, _| {
1099 if let Some(state) = this.get_language_server_state(server_id) {
1100 state.trace_level = level;
1101 }
1102 });
1103
1104 server
1105 .notify::<SetTrace>(&SetTraceParams { value: level })
1106 .ok();
1107 }
1108 }
1109
1110 fn show_server_info(
1111 &mut self,
1112 server_id: LanguageServerId,
1113 window: &mut Window,
1114 cx: &mut Context<Self>,
1115 ) {
1116 let lsp_store = self.project.read(cx).lsp_store();
1117 let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
1118 return;
1119 };
1120 self.current_server_id = Some(server_id);
1121 self.active_entry_kind = LogKind::ServerInfo;
1122 let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx);
1123 self.editor = editor;
1124 self.editor_subscriptions = editor_subscriptions;
1125 cx.notify();
1126 self.editor.read(cx).focus_handle(cx).focus(window);
1127 }
1128}
1129
1130fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
1131 lines
1132 .iter()
1133 .filter(|message| message.should_include(level))
1134 .flat_map(|message| [message.as_ref(), "\n"])
1135 .collect()
1136}
1137
1138impl Render for LspLogView {
1139 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1140 self.editor.update(cx, |editor, cx| {
1141 editor.render(window, cx).into_any_element()
1142 })
1143 }
1144}
1145
1146impl Focusable for LspLogView {
1147 fn focus_handle(&self, _: &App) -> FocusHandle {
1148 self.focus_handle.clone()
1149 }
1150}
1151
1152impl Item for LspLogView {
1153 type Event = EditorEvent;
1154
1155 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1156 Editor::to_item_events(event, f)
1157 }
1158
1159 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1160 "LSP Logs".into()
1161 }
1162
1163 fn telemetry_event_text(&self) -> Option<&'static str> {
1164 None
1165 }
1166
1167 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1168 Some(Box::new(handle.clone()))
1169 }
1170
1171 fn act_as_type<'a>(
1172 &'a self,
1173 type_id: TypeId,
1174 self_handle: &'a Entity<Self>,
1175 _: &'a App,
1176 ) -> Option<AnyView> {
1177 if type_id == TypeId::of::<Self>() {
1178 Some(self_handle.to_any())
1179 } else if type_id == TypeId::of::<Editor>() {
1180 Some(self.editor.to_any())
1181 } else {
1182 None
1183 }
1184 }
1185
1186 fn clone_on_split(
1187 &self,
1188 _workspace_id: Option<WorkspaceId>,
1189 window: &mut Window,
1190 cx: &mut Context<Self>,
1191 ) -> Option<Entity<Self>>
1192 where
1193 Self: Sized,
1194 {
1195 Some(cx.new(|cx| {
1196 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
1197 if let Some(server_id) = self.current_server_id {
1198 match self.active_entry_kind {
1199 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
1200 LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
1201 LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
1202 LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
1203 }
1204 }
1205 new_view
1206 }))
1207 }
1208}
1209
1210impl SearchableItem for LspLogView {
1211 type Match = <Editor as SearchableItem>::Match;
1212
1213 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1214 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1215 }
1216
1217 fn update_matches(
1218 &mut self,
1219 matches: &[Self::Match],
1220 window: &mut Window,
1221 cx: &mut Context<Self>,
1222 ) {
1223 self.editor
1224 .update(cx, |e, cx| e.update_matches(matches, window, cx))
1225 }
1226
1227 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1228 self.editor
1229 .update(cx, |e, cx| e.query_suggestion(window, cx))
1230 }
1231
1232 fn activate_match(
1233 &mut self,
1234 index: usize,
1235 matches: &[Self::Match],
1236 window: &mut Window,
1237 cx: &mut Context<Self>,
1238 ) {
1239 self.editor
1240 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1241 }
1242
1243 fn select_matches(
1244 &mut self,
1245 matches: &[Self::Match],
1246 window: &mut Window,
1247 cx: &mut Context<Self>,
1248 ) {
1249 self.editor
1250 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1251 }
1252
1253 fn find_matches(
1254 &mut self,
1255 query: Arc<project::search::SearchQuery>,
1256 window: &mut Window,
1257 cx: &mut Context<Self>,
1258 ) -> gpui::Task<Vec<Self::Match>> {
1259 self.editor
1260 .update(cx, |e, cx| e.find_matches(query, window, cx))
1261 }
1262
1263 fn replace(
1264 &mut self,
1265 _: &Self::Match,
1266 _: &SearchQuery,
1267 _window: &mut Window,
1268 _: &mut Context<Self>,
1269 ) {
1270 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1271 }
1272 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1273 workspace::searchable::SearchOptions {
1274 case: true,
1275 word: true,
1276 regex: true,
1277 find_in_results: false,
1278 // LSP log is read-only.
1279 replacement: false,
1280 selection: false,
1281 }
1282 }
1283 fn active_match_index(
1284 &mut self,
1285 direction: Direction,
1286 matches: &[Self::Match],
1287 window: &mut Window,
1288 cx: &mut Context<Self>,
1289 ) -> Option<usize> {
1290 self.editor.update(cx, |e, cx| {
1291 e.active_match_index(direction, matches, window, cx)
1292 })
1293 }
1294}
1295
1296impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1297
1298impl ToolbarItemView for LspLogToolbarItemView {
1299 fn set_active_pane_item(
1300 &mut self,
1301 active_pane_item: Option<&dyn ItemHandle>,
1302 _: &mut Window,
1303 cx: &mut Context<Self>,
1304 ) -> workspace::ToolbarItemLocation {
1305 if let Some(item) = active_pane_item
1306 && let Some(log_view) = item.downcast::<LspLogView>()
1307 {
1308 self.log_view = Some(log_view.clone());
1309 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1310 cx.notify();
1311 }));
1312 return ToolbarItemLocation::PrimaryLeft;
1313 }
1314 self.log_view = None;
1315 self._log_view_subscription = None;
1316 ToolbarItemLocation::Hidden
1317 }
1318}
1319
1320impl Render for LspLogToolbarItemView {
1321 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1322 let Some(log_view) = self.log_view.clone() else {
1323 return div();
1324 };
1325
1326 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1327 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1328 let current_server_id = log_view.current_server_id;
1329 (menu_rows, current_server_id)
1330 });
1331
1332 let current_server = current_server_id.and_then(|current_server_id| {
1333 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1334 Some(menu_rows[ix].clone())
1335 } else {
1336 None
1337 }
1338 });
1339
1340 let available_language_servers: Vec<_> = menu_rows
1341 .into_iter()
1342 .map(|row| {
1343 (
1344 row.server_id,
1345 row.server_name,
1346 row.worktree_root_name,
1347 row.selected_entry,
1348 )
1349 })
1350 .collect();
1351
1352 let log_toolbar_view = cx.entity();
1353
1354 let lsp_menu = PopoverMenu::new("LspLogView")
1355 .anchor(Corner::TopLeft)
1356 .trigger(
1357 Button::new(
1358 "language_server_menu_header",
1359 current_server
1360 .as_ref()
1361 .map(|row| {
1362 Cow::Owned(format!(
1363 "{} ({})",
1364 row.server_name.0, row.worktree_root_name,
1365 ))
1366 })
1367 .unwrap_or_else(|| "No server selected".into()),
1368 )
1369 .icon(IconName::ChevronDown)
1370 .icon_size(IconSize::Small)
1371 .icon_color(Color::Muted),
1372 )
1373 .menu({
1374 let log_view = log_view.clone();
1375 move |window, cx| {
1376 let log_view = log_view.clone();
1377 ContextMenu::build(window, cx, |mut menu, window, _| {
1378 for (server_id, name, worktree_root, active_entry_kind) in
1379 available_language_servers.iter()
1380 {
1381 let label = format!("{} ({})", name, worktree_root);
1382 let server_id = *server_id;
1383 let active_entry_kind = *active_entry_kind;
1384 menu = menu.entry(
1385 label,
1386 None,
1387 window.handler_for(&log_view, move |view, window, cx| {
1388 view.current_server_id = Some(server_id);
1389 view.active_entry_kind = active_entry_kind;
1390 match view.active_entry_kind {
1391 LogKind::Rpc => {
1392 view.toggle_rpc_trace_for_server(
1393 server_id, true, window, cx,
1394 );
1395 view.show_rpc_trace_for_server(server_id, window, cx);
1396 }
1397 LogKind::Trace => {
1398 view.show_trace_for_server(server_id, window, cx)
1399 }
1400 LogKind::Logs => {
1401 view.show_logs_for_server(server_id, window, cx)
1402 }
1403 LogKind::ServerInfo => {
1404 view.show_server_info(server_id, window, cx)
1405 }
1406 }
1407 cx.notify();
1408 }),
1409 );
1410 }
1411 menu
1412 })
1413 .into()
1414 }
1415 });
1416
1417 let view_selector = current_server.map(|server| {
1418 let server_id = server.server_id;
1419 let is_remote = server.server_kind.is_remote();
1420 let rpc_trace_enabled = server.rpc_trace_enabled;
1421 let log_view = log_view.clone();
1422 PopoverMenu::new("LspViewSelector")
1423 .anchor(Corner::TopLeft)
1424 .trigger(
1425 Button::new("language_server_menu_header", server.selected_entry.label())
1426 .icon(IconName::ChevronDown)
1427 .icon_size(IconSize::Small)
1428 .icon_color(Color::Muted),
1429 )
1430 .menu(move |window, cx| {
1431 let log_toolbar_view = log_toolbar_view.clone();
1432 let log_view = log_view.clone();
1433 Some(ContextMenu::build(window, cx, move |this, window, _| {
1434 this.entry(
1435 SERVER_LOGS,
1436 None,
1437 window.handler_for(&log_view, move |view, window, cx| {
1438 view.show_logs_for_server(server_id, window, cx);
1439 }),
1440 )
1441 .when(!is_remote, |this| {
1442 this.entry(
1443 SERVER_TRACE,
1444 None,
1445 window.handler_for(&log_view, move |view, window, cx| {
1446 view.show_trace_for_server(server_id, window, cx);
1447 }),
1448 )
1449 .custom_entry(
1450 {
1451 let log_toolbar_view = log_toolbar_view.clone();
1452 move |window, _| {
1453 h_flex()
1454 .w_full()
1455 .justify_between()
1456 .child(Label::new(RPC_MESSAGES))
1457 .child(
1458 div().child(
1459 Checkbox::new(
1460 "LspLogEnableRpcTrace",
1461 if rpc_trace_enabled {
1462 ToggleState::Selected
1463 } else {
1464 ToggleState::Unselected
1465 },
1466 )
1467 .on_click(window.listener_for(
1468 &log_toolbar_view,
1469 move |view, selection, window, cx| {
1470 let enabled = matches!(
1471 selection,
1472 ToggleState::Selected
1473 );
1474 view.toggle_rpc_logging_for_server(
1475 server_id, enabled, window, cx,
1476 );
1477 cx.stop_propagation();
1478 },
1479 )),
1480 ),
1481 )
1482 .into_any_element()
1483 }
1484 },
1485 window.handler_for(&log_view, move |view, window, cx| {
1486 view.show_rpc_trace_for_server(server_id, window, cx);
1487 }),
1488 )
1489 })
1490 .entry(
1491 SERVER_INFO,
1492 None,
1493 window.handler_for(&log_view, move |view, window, cx| {
1494 view.show_server_info(server_id, window, cx);
1495 }),
1496 )
1497 }))
1498 })
1499 });
1500
1501 h_flex()
1502 .size_full()
1503 .gap_1()
1504 .justify_between()
1505 .child(
1506 h_flex()
1507 .gap_0p5()
1508 .child(lsp_menu)
1509 .children(view_selector)
1510 .child(
1511 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1512 LogKind::Trace => {
1513 let log_view = log_view.clone();
1514 div().child(
1515 PopoverMenu::new("lsp-trace-level-menu")
1516 .anchor(Corner::TopLeft)
1517 .trigger(
1518 Button::new(
1519 "language_server_trace_level_selector",
1520 "Trace level",
1521 )
1522 .icon(IconName::ChevronDown)
1523 .icon_size(IconSize::Small)
1524 .icon_color(Color::Muted),
1525 )
1526 .menu({
1527 let log_view = log_view;
1528
1529 move |window, cx| {
1530 let id = log_view.read(cx).current_server_id?;
1531
1532 let trace_level =
1533 log_view.update(cx, |this, cx| {
1534 this.log_store.update(cx, |this, _| {
1535 Some(
1536 this.get_language_server_state(id)?
1537 .trace_level,
1538 )
1539 })
1540 })?;
1541
1542 ContextMenu::build(
1543 window,
1544 cx,
1545 |mut menu, window, cx| {
1546 let log_view = log_view.clone();
1547
1548 for (option, label) in [
1549 (TraceValue::Off, "Off"),
1550 (TraceValue::Messages, "Messages"),
1551 (TraceValue::Verbose, "Verbose"),
1552 ] {
1553 menu = menu.entry(label, None, {
1554 let log_view = log_view.clone();
1555 move |_, cx| {
1556 log_view.update(cx, |this, cx| {
1557 if let Some(id) =
1558 this.current_server_id
1559 {
1560 this.update_trace_level(
1561 id, option, cx,
1562 );
1563 }
1564 });
1565 }
1566 });
1567 if option == trace_level {
1568 menu.select_last(window, cx);
1569 }
1570 }
1571
1572 menu
1573 },
1574 )
1575 .into()
1576 }
1577 }),
1578 )
1579 }
1580 LogKind::Logs => {
1581 let log_view = log_view.clone();
1582 div().child(
1583 PopoverMenu::new("lsp-log-level-menu")
1584 .anchor(Corner::TopLeft)
1585 .trigger(
1586 Button::new(
1587 "language_server_log_level_selector",
1588 "Log level",
1589 )
1590 .icon(IconName::ChevronDown)
1591 .icon_size(IconSize::Small)
1592 .icon_color(Color::Muted),
1593 )
1594 .menu({
1595 let log_view = log_view;
1596
1597 move |window, cx| {
1598 let id = log_view.read(cx).current_server_id?;
1599
1600 let log_level =
1601 log_view.update(cx, |this, cx| {
1602 this.log_store.update(cx, |this, _| {
1603 Some(
1604 this.get_language_server_state(id)?
1605 .log_level,
1606 )
1607 })
1608 })?;
1609
1610 ContextMenu::build(
1611 window,
1612 cx,
1613 |mut menu, window, cx| {
1614 let log_view = log_view.clone();
1615
1616 for (option, label) in [
1617 (MessageType::LOG, "Log"),
1618 (MessageType::INFO, "Info"),
1619 (MessageType::WARNING, "Warning"),
1620 (MessageType::ERROR, "Error"),
1621 ] {
1622 menu = menu.entry(label, None, {
1623 let log_view = log_view.clone();
1624 move |window, cx| {
1625 log_view.update(cx, |this, cx| {
1626 if let Some(id) =
1627 this.current_server_id
1628 {
1629 this.update_log_level(
1630 id, option, window, cx,
1631 );
1632 }
1633 });
1634 }
1635 });
1636 if option == log_level {
1637 menu.select_last(window, cx);
1638 }
1639 }
1640
1641 menu
1642 },
1643 )
1644 .into()
1645 }
1646 }),
1647 )
1648 }
1649 _ => div(),
1650 }),
1651 ),
1652 )
1653 .child(
1654 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1655 |this, _, window, cx| {
1656 if let Some(log_view) = this.log_view.as_ref() {
1657 log_view.update(cx, |log_view, cx| {
1658 log_view.editor.update(cx, |editor, cx| {
1659 editor.set_read_only(false);
1660 editor.clear(window, cx);
1661 editor.set_read_only(true);
1662 });
1663 })
1664 }
1665 },
1666 )),
1667 )
1668 }
1669}
1670
1671fn initialize_new_editor(
1672 content: String,
1673 move_to_end: bool,
1674 window: &mut Window,
1675 cx: &mut App,
1676) -> Entity<Editor> {
1677 cx.new(|cx| {
1678 let mut editor = Editor::multi_line(window, cx);
1679 editor.hide_minimap_by_default(window, cx);
1680 editor.set_text(content, window, cx);
1681 editor.set_show_git_diff_gutter(false, cx);
1682 editor.set_show_runnables(false, cx);
1683 editor.set_show_breakpoints(false, cx);
1684 editor.set_read_only(true);
1685 editor.set_show_edit_predictions(Some(false), window, cx);
1686 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1687 if move_to_end {
1688 editor.move_to_end(&MoveToEnd, window, cx);
1689 }
1690 editor
1691 })
1692}
1693
1694const RPC_MESSAGES: &str = "RPC Messages";
1695const SERVER_LOGS: &str = "Server Logs";
1696const SERVER_TRACE: &str = "Server Trace";
1697const SERVER_INFO: &str = "Server Info";
1698
1699impl Default for LspLogToolbarItemView {
1700 fn default() -> Self {
1701 Self::new()
1702 }
1703}
1704
1705impl LspLogToolbarItemView {
1706 pub fn new() -> Self {
1707 Self {
1708 log_view: None,
1709 _log_view_subscription: None,
1710 }
1711 }
1712
1713 fn toggle_rpc_logging_for_server(
1714 &mut self,
1715 id: LanguageServerId,
1716 enabled: bool,
1717 window: &mut Window,
1718 cx: &mut Context<Self>,
1719 ) {
1720 if let Some(log_view) = &self.log_view {
1721 log_view.update(cx, |log_view, cx| {
1722 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1723 if !enabled && Some(id) == log_view.current_server_id {
1724 log_view.show_logs_for_server(id, window, cx);
1725 cx.notify();
1726 } else if enabled {
1727 log_view.show_rpc_trace_for_server(id, window, cx);
1728 cx.notify();
1729 }
1730 window.focus(&log_view.focus_handle);
1731 });
1732 }
1733 cx.notify();
1734 }
1735}
1736
1737pub enum Event {
1738 NewServerLogEntry {
1739 id: LanguageServerId,
1740 kind: LogKind,
1741 text: String,
1742 },
1743}
1744
1745impl EventEmitter<Event> for LogStore {}
1746impl EventEmitter<EditorEvent> for LspLogView {}
1747impl EventEmitter<SearchEvent> for LspLogView {}