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