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
410 .clone()
411 .filter(|_| server_state.io_logs_subscription.is_none())
412 {
413 let io_tx = self.io_tx.clone();
414 let server_id = server.server_id();
415 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
416 io_tx
417 .unbounded_send((server_id, io_kind, message.to_string()))
418 .ok();
419 }));
420 }
421
422 Some(server_state)
423 }
424
425 fn add_language_server_log(
426 &mut self,
427 id: LanguageServerId,
428 typ: MessageType,
429 message: &str,
430 cx: &mut Context<Self>,
431 ) -> Option<()> {
432 let language_server_state = self.get_language_server_state(id)?;
433
434 let log_lines = &mut language_server_state.log_messages;
435 Self::add_language_server_message(
436 log_lines,
437 id,
438 LogMessage {
439 message: message.trim_end().to_string(),
440 typ,
441 },
442 language_server_state.log_level,
443 LogKind::Logs,
444 cx,
445 );
446 Some(())
447 }
448
449 fn add_language_server_trace(
450 &mut self,
451 id: LanguageServerId,
452 message: &str,
453 cx: &mut Context<Self>,
454 ) -> Option<()> {
455 let language_server_state = self.get_language_server_state(id)?;
456
457 let log_lines = &mut language_server_state.trace_messages;
458 Self::add_language_server_message(
459 log_lines,
460 id,
461 TraceMessage {
462 message: message.trim().to_string(),
463 },
464 (),
465 LogKind::Trace,
466 cx,
467 );
468 Some(())
469 }
470
471 fn add_language_server_message<T: Message>(
472 log_lines: &mut VecDeque<T>,
473 id: LanguageServerId,
474 message: T,
475 current_severity: <T as Message>::Level,
476 kind: LogKind,
477 cx: &mut Context<Self>,
478 ) {
479 while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
480 log_lines.pop_front();
481 }
482 let text = message.as_ref().to_string();
483 let visible = message.should_include(current_severity);
484 log_lines.push_back(message);
485
486 if visible {
487 cx.emit(Event::NewServerLogEntry { id, kind, text });
488 cx.notify();
489 }
490 }
491
492 fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
493 self.language_servers.remove(&id);
494 cx.notify();
495 }
496
497 pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
498 Some(&self.language_servers.get(&server_id)?.log_messages)
499 }
500
501 pub(super) fn server_trace(
502 &self,
503 server_id: LanguageServerId,
504 ) -> Option<&VecDeque<TraceMessage>> {
505 Some(&self.language_servers.get(&server_id)?.trace_messages)
506 }
507
508 fn server_ids_for_project<'a>(
509 &'a self,
510 lookup_project: &'a WeakEntity<Project>,
511 ) -> impl Iterator<Item = LanguageServerId> + 'a {
512 self.language_servers
513 .iter()
514 .filter_map(move |(id, state)| match &state.kind {
515 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
516 if project == lookup_project {
517 Some(*id)
518 } else {
519 None
520 }
521 }
522 LanguageServerKind::Global => Some(*id),
523 })
524 }
525
526 fn enable_rpc_trace_for_language_server(
527 &mut self,
528 server_id: LanguageServerId,
529 ) -> Option<&mut LanguageServerRpcState> {
530 let rpc_state = self
531 .language_servers
532 .get_mut(&server_id)?
533 .rpc_state
534 .get_or_insert_with(|| LanguageServerRpcState {
535 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
536 last_message_kind: None,
537 });
538 Some(rpc_state)
539 }
540
541 pub fn disable_rpc_trace_for_language_server(
542 &mut self,
543 server_id: LanguageServerId,
544 ) -> Option<()> {
545 self.language_servers.get_mut(&server_id)?.rpc_state.take();
546 Some(())
547 }
548
549 pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
550 match server {
551 LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
552 LanguageServerSelector::Name(name) => self
553 .language_servers
554 .iter()
555 .any(|(_, state)| state.name.as_ref() == Some(name)),
556 }
557 }
558
559 pub fn open_server_log(
560 &mut self,
561 workspace: WeakEntity<Workspace>,
562 server: LanguageServerSelector,
563 window: &mut Window,
564 cx: &mut Context<Self>,
565 ) {
566 cx.spawn_in(window, async move |log_store, cx| {
567 let Some(log_store) = log_store.upgrade() else {
568 return;
569 };
570 workspace
571 .update_in(cx, |workspace, window, cx| {
572 let project = workspace.project().clone();
573 let tool_log_store = log_store.clone();
574 let log_view = get_or_create_tool(
575 workspace,
576 SplitDirection::Right,
577 window,
578 cx,
579 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
580 );
581 log_view.update(cx, |log_view, cx| {
582 let server_id = match server {
583 LanguageServerSelector::Id(id) => Some(id),
584 LanguageServerSelector::Name(name) => {
585 log_store.read(cx).language_servers.iter().find_map(
586 |(id, state)| {
587 if state.name.as_ref() == Some(&name) {
588 Some(*id)
589 } else {
590 None
591 }
592 },
593 )
594 }
595 };
596 if let Some(server_id) = server_id {
597 log_view.show_logs_for_server(server_id, window, cx);
598 }
599 });
600 })
601 .ok();
602 })
603 .detach();
604 }
605
606 pub fn open_server_trace(
607 &mut self,
608 workspace: WeakEntity<Workspace>,
609 server: LanguageServerSelector,
610 window: &mut Window,
611 cx: &mut Context<Self>,
612 ) {
613 cx.spawn_in(window, async move |log_store, cx| {
614 let Some(log_store) = log_store.upgrade() else {
615 return;
616 };
617 workspace
618 .update_in(cx, |workspace, window, cx| {
619 let project = workspace.project().clone();
620 let tool_log_store = log_store.clone();
621 let log_view = get_or_create_tool(
622 workspace,
623 SplitDirection::Right,
624 window,
625 cx,
626 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
627 );
628 log_view.update(cx, |log_view, cx| {
629 let server_id = match server {
630 LanguageServerSelector::Id(id) => Some(id),
631 LanguageServerSelector::Name(name) => {
632 log_store.read(cx).language_servers.iter().find_map(
633 |(id, state)| {
634 if state.name.as_ref() == Some(&name) {
635 Some(*id)
636 } else {
637 None
638 }
639 },
640 )
641 }
642 };
643 if let Some(server_id) = server_id {
644 log_view.show_rpc_trace_for_server(server_id, window, cx);
645 }
646 });
647 })
648 .ok();
649 })
650 .detach();
651 }
652
653 fn on_io(
654 &mut self,
655 language_server_id: LanguageServerId,
656 io_kind: IoKind,
657 message: &str,
658 cx: &mut Context<Self>,
659 ) -> Option<()> {
660 let is_received = match io_kind {
661 IoKind::StdOut => true,
662 IoKind::StdIn => false,
663 IoKind::StdErr => {
664 self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
665 return Some(());
666 }
667 };
668
669 let state = self
670 .get_language_server_state(language_server_id)?
671 .rpc_state
672 .as_mut()?;
673 let kind = if is_received {
674 MessageKind::Receive
675 } else {
676 MessageKind::Send
677 };
678
679 let rpc_log_lines = &mut state.rpc_messages;
680 if state.last_message_kind != Some(kind) {
681 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
682 rpc_log_lines.pop_front();
683 }
684 let line_before_message = match kind {
685 MessageKind::Send => SEND_LINE,
686 MessageKind::Receive => RECEIVE_LINE,
687 };
688 rpc_log_lines.push_back(RpcMessage {
689 message: line_before_message.to_string(),
690 });
691 cx.emit(Event::NewServerLogEntry {
692 id: language_server_id,
693 kind: LogKind::Rpc,
694 text: line_before_message.to_string(),
695 });
696 }
697
698 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
699 rpc_log_lines.pop_front();
700 }
701
702 let message = message.trim();
703 rpc_log_lines.push_back(RpcMessage {
704 message: message.to_string(),
705 });
706 cx.emit(Event::NewServerLogEntry {
707 id: language_server_id,
708 kind: LogKind::Rpc,
709 text: message.to_string(),
710 });
711 cx.notify();
712 Some(())
713 }
714}
715
716impl LspLogView {
717 pub fn new(
718 project: Entity<Project>,
719 log_store: Entity<LogStore>,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 ) -> Self {
723 let server_id = log_store
724 .read(cx)
725 .language_servers
726 .iter()
727 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
728 .map(|(id, _)| *id);
729
730 let weak_project = project.downgrade();
731 let model_changes_subscription =
732 cx.observe_in(&log_store, window, move |this, store, window, cx| {
733 let first_server_id_for_project =
734 store.read(cx).server_ids_for_project(&weak_project).next();
735 if let Some(current_lsp) = this.current_server_id {
736 if !store.read(cx).language_servers.contains_key(¤t_lsp)
737 && let Some(server_id) = first_server_id_for_project
738 {
739 match this.active_entry_kind {
740 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
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 {
782 editor.fold_ranges(
783 vec![last_offset + fold_offset..last_offset + text.len()],
784 false,
785 window,
786 cx,
787 );
788 }
789
790 if newest_cursor_is_at_end {
791 editor.request_autoscroll(Autoscroll::bottom(), cx);
792 }
793 editor.set_read_only(true);
794 });
795 }
796 }
797 },
798 );
799 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
800
801 let focus_handle = cx.focus_handle();
802 let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
803 window.focus(&log_view.editor.focus_handle(cx));
804 });
805
806 let mut this = Self {
807 focus_handle,
808 editor,
809 editor_subscriptions,
810 project,
811 log_store,
812 current_server_id: None,
813 active_entry_kind: LogKind::Logs,
814 _log_store_subscriptions: vec![
815 model_changes_subscription,
816 events_subscriptions,
817 focus_subscription,
818 ],
819 };
820 if let Some(server_id) = server_id {
821 this.show_logs_for_server(server_id, window, cx);
822 }
823 this
824 }
825
826 fn editor_for_logs(
827 log_contents: String,
828 window: &mut Window,
829 cx: &mut Context<Self>,
830 ) -> (Entity<Editor>, Vec<Subscription>) {
831 let editor = initialize_new_editor(log_contents, true, window, cx);
832 let editor_subscription = cx.subscribe(
833 &editor,
834 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
835 );
836 let search_subscription = cx.subscribe(
837 &editor,
838 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
839 );
840 (editor, vec![editor_subscription, search_subscription])
841 }
842
843 fn editor_for_server_info(
844 server: &LanguageServer,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) -> (Entity<Editor>, Vec<Subscription>) {
848 let server_info = format!(
849 "* Server: {NAME} (id {ID})
850
851* Binary: {BINARY:#?}
852
853* Registered workspace folders:
854{WORKSPACE_FOLDERS}
855
856* Capabilities: {CAPABILITIES}
857
858* Configuration: {CONFIGURATION}",
859 NAME = server.name(),
860 ID = server.server_id(),
861 BINARY = server.binary(),
862 WORKSPACE_FOLDERS = server
863 .workspace_folders()
864 .into_iter()
865 .filter_map(|path| path
866 .to_file_path()
867 .ok()
868 .map(|path| path.to_string_lossy().into_owned()))
869 .collect::<Vec<_>>()
870 .join(", "),
871 CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
872 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
873 CONFIGURATION = serde_json::to_string_pretty(server.configuration())
874 .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
875 );
876 let editor = initialize_new_editor(server_info, false, window, cx);
877 let editor_subscription = cx.subscribe(
878 &editor,
879 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
880 );
881 let search_subscription = cx.subscribe(
882 &editor,
883 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
884 );
885 (editor, vec![editor_subscription, search_subscription])
886 }
887
888 pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
889 let log_store = self.log_store.read(cx);
890
891 let unknown_server = LanguageServerName::new_static("unknown server");
892
893 let mut rows = log_store
894 .language_servers
895 .iter()
896 .map(|(server_id, state)| match &state.kind {
897 LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
898 let worktree_root_name = state
899 .worktree_id
900 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
901 .map(|worktree| worktree.read(cx).root_name().to_string())
902 .unwrap_or_else(|| "Unknown worktree".to_string());
903
904 LogMenuItem {
905 server_id: *server_id,
906 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
907 server_kind: state.kind.clone(),
908 worktree_root_name,
909 rpc_trace_enabled: state.rpc_state.is_some(),
910 selected_entry: self.active_entry_kind,
911 trace_level: lsp::TraceValue::Off,
912 }
913 }
914
915 LanguageServerKind::Global => LogMenuItem {
916 server_id: *server_id,
917 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
918 server_kind: state.kind.clone(),
919 worktree_root_name: "supplementary".to_string(),
920 rpc_trace_enabled: state.rpc_state.is_some(),
921 selected_entry: self.active_entry_kind,
922 trace_level: lsp::TraceValue::Off,
923 },
924 })
925 .chain(
926 self.project
927 .read(cx)
928 .supplementary_language_servers(cx)
929 .filter_map(|(server_id, name)| {
930 let state = log_store.language_servers.get(&server_id)?;
931 Some(LogMenuItem {
932 server_id,
933 server_name: name.clone(),
934 server_kind: state.kind.clone(),
935 worktree_root_name: "supplementary".to_string(),
936 rpc_trace_enabled: state.rpc_state.is_some(),
937 selected_entry: self.active_entry_kind,
938 trace_level: lsp::TraceValue::Off,
939 })
940 }),
941 )
942 .collect::<Vec<_>>();
943 rows.sort_by_key(|row| row.server_id);
944 rows.dedup_by_key(|row| row.server_id);
945 Some(rows)
946 }
947
948 fn show_logs_for_server(
949 &mut self,
950 server_id: LanguageServerId,
951 window: &mut Window,
952 cx: &mut Context<Self>,
953 ) {
954 let typ = self
955 .log_store
956 .read(cx)
957 .language_servers
958 .get(&server_id)
959 .map(|v| v.log_level)
960 .unwrap_or(MessageType::LOG);
961 let log_contents = self
962 .log_store
963 .read(cx)
964 .server_logs(server_id)
965 .map(|v| log_contents(v, typ));
966 if let Some(log_contents) = log_contents {
967 self.current_server_id = Some(server_id);
968 self.active_entry_kind = LogKind::Logs;
969 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
970 self.editor = editor;
971 self.editor_subscriptions = editor_subscriptions;
972 cx.notify();
973 }
974 self.editor.read(cx).focus_handle(cx).focus(window);
975 }
976
977 fn update_log_level(
978 &self,
979 server_id: LanguageServerId,
980 level: MessageType,
981 window: &mut Window,
982 cx: &mut Context<Self>,
983 ) {
984 let log_contents = self.log_store.update(cx, |this, _| {
985 if let Some(state) = this.get_language_server_state(server_id) {
986 state.log_level = level;
987 }
988
989 this.server_logs(server_id).map(|v| log_contents(v, level))
990 });
991
992 if let Some(log_contents) = log_contents {
993 self.editor.update(cx, |editor, cx| {
994 editor.set_text(log_contents, window, cx);
995 editor.move_to_end(&MoveToEnd, window, cx);
996 });
997 cx.notify();
998 }
999
1000 self.editor.read(cx).focus_handle(cx).focus(window);
1001 }
1002
1003 fn show_trace_for_server(
1004 &mut self,
1005 server_id: LanguageServerId,
1006 window: &mut Window,
1007 cx: &mut Context<Self>,
1008 ) {
1009 let log_contents = self
1010 .log_store
1011 .read(cx)
1012 .server_trace(server_id)
1013 .map(|v| log_contents(v, ()));
1014 if let Some(log_contents) = log_contents {
1015 self.current_server_id = Some(server_id);
1016 self.active_entry_kind = LogKind::Trace;
1017 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
1018 self.editor = editor;
1019 self.editor_subscriptions = editor_subscriptions;
1020 cx.notify();
1021 }
1022 self.editor.read(cx).focus_handle(cx).focus(window);
1023 }
1024
1025 fn show_rpc_trace_for_server(
1026 &mut self,
1027 server_id: LanguageServerId,
1028 window: &mut Window,
1029 cx: &mut Context<Self>,
1030 ) {
1031 let rpc_log = self.log_store.update(cx, |log_store, _| {
1032 log_store
1033 .enable_rpc_trace_for_language_server(server_id)
1034 .map(|state| log_contents(&state.rpc_messages, ()))
1035 });
1036 if let Some(rpc_log) = rpc_log {
1037 self.current_server_id = Some(server_id);
1038 self.active_entry_kind = LogKind::Rpc;
1039 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
1040 let language = self.project.read(cx).languages().language_for_name("JSON");
1041 editor
1042 .read(cx)
1043 .buffer()
1044 .read(cx)
1045 .as_singleton()
1046 .expect("log buffer should be a singleton")
1047 .update(cx, |_, cx| {
1048 cx.spawn({
1049 let buffer = cx.entity();
1050 async move |_, cx| {
1051 let language = language.await.ok();
1052 buffer.update(cx, |buffer, cx| {
1053 buffer.set_language(language, cx);
1054 })
1055 }
1056 })
1057 .detach_and_log_err(cx);
1058 });
1059
1060 self.editor = editor;
1061 self.editor_subscriptions = editor_subscriptions;
1062 cx.notify();
1063 }
1064
1065 self.editor.read(cx).focus_handle(cx).focus(window);
1066 }
1067
1068 fn toggle_rpc_trace_for_server(
1069 &mut self,
1070 server_id: LanguageServerId,
1071 enabled: bool,
1072 window: &mut Window,
1073 cx: &mut Context<Self>,
1074 ) {
1075 self.log_store.update(cx, |log_store, _| {
1076 if enabled {
1077 log_store.enable_rpc_trace_for_language_server(server_id);
1078 } else {
1079 log_store.disable_rpc_trace_for_language_server(server_id);
1080 }
1081 });
1082 if !enabled && Some(server_id) == self.current_server_id {
1083 self.show_logs_for_server(server_id, window, cx);
1084 cx.notify();
1085 }
1086 }
1087
1088 fn update_trace_level(
1089 &self,
1090 server_id: LanguageServerId,
1091 level: TraceValue,
1092 cx: &mut Context<Self>,
1093 ) {
1094 if let Some(server) = self
1095 .project
1096 .read(cx)
1097 .lsp_store()
1098 .read(cx)
1099 .language_server_for_id(server_id)
1100 {
1101 self.log_store.update(cx, |this, _| {
1102 if let Some(state) = this.get_language_server_state(server_id) {
1103 state.trace_level = level;
1104 }
1105 });
1106
1107 server
1108 .notify::<SetTrace>(&SetTraceParams { value: level })
1109 .ok();
1110 }
1111 }
1112
1113 fn show_server_info(
1114 &mut self,
1115 server_id: LanguageServerId,
1116 window: &mut Window,
1117 cx: &mut Context<Self>,
1118 ) {
1119 let lsp_store = self.project.read(cx).lsp_store();
1120 let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
1121 return;
1122 };
1123 self.current_server_id = Some(server_id);
1124 self.active_entry_kind = LogKind::ServerInfo;
1125 let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx);
1126 self.editor = editor;
1127 self.editor_subscriptions = editor_subscriptions;
1128 cx.notify();
1129 self.editor.read(cx).focus_handle(cx).focus(window);
1130 }
1131}
1132
1133fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
1134 lines
1135 .iter()
1136 .filter(|message| message.should_include(level))
1137 .flat_map(|message| [message.as_ref(), "\n"])
1138 .collect()
1139}
1140
1141impl Render for LspLogView {
1142 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1143 self.editor.update(cx, |editor, cx| {
1144 editor.render(window, cx).into_any_element()
1145 })
1146 }
1147}
1148
1149impl Focusable for LspLogView {
1150 fn focus_handle(&self, _: &App) -> FocusHandle {
1151 self.focus_handle.clone()
1152 }
1153}
1154
1155impl Item for LspLogView {
1156 type Event = EditorEvent;
1157
1158 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1159 Editor::to_item_events(event, f)
1160 }
1161
1162 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1163 "LSP Logs".into()
1164 }
1165
1166 fn telemetry_event_text(&self) -> Option<&'static str> {
1167 None
1168 }
1169
1170 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1171 Some(Box::new(handle.clone()))
1172 }
1173
1174 fn act_as_type<'a>(
1175 &'a self,
1176 type_id: TypeId,
1177 self_handle: &'a Entity<Self>,
1178 _: &'a App,
1179 ) -> Option<AnyView> {
1180 if type_id == TypeId::of::<Self>() {
1181 Some(self_handle.to_any())
1182 } else if type_id == TypeId::of::<Editor>() {
1183 Some(self.editor.to_any())
1184 } else {
1185 None
1186 }
1187 }
1188
1189 fn clone_on_split(
1190 &self,
1191 _workspace_id: Option<WorkspaceId>,
1192 window: &mut Window,
1193 cx: &mut Context<Self>,
1194 ) -> Option<Entity<Self>>
1195 where
1196 Self: Sized,
1197 {
1198 Some(cx.new(|cx| {
1199 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
1200 if let Some(server_id) = self.current_server_id {
1201 match self.active_entry_kind {
1202 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
1203 LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
1204 LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
1205 LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
1206 }
1207 }
1208 new_view
1209 }))
1210 }
1211}
1212
1213impl SearchableItem for LspLogView {
1214 type Match = <Editor as SearchableItem>::Match;
1215
1216 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1217 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1218 }
1219
1220 fn update_matches(
1221 &mut self,
1222 matches: &[Self::Match],
1223 window: &mut Window,
1224 cx: &mut Context<Self>,
1225 ) {
1226 self.editor
1227 .update(cx, |e, cx| e.update_matches(matches, window, cx))
1228 }
1229
1230 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1231 self.editor
1232 .update(cx, |e, cx| e.query_suggestion(window, cx))
1233 }
1234
1235 fn activate_match(
1236 &mut self,
1237 index: usize,
1238 matches: &[Self::Match],
1239 window: &mut Window,
1240 cx: &mut Context<Self>,
1241 ) {
1242 self.editor
1243 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1244 }
1245
1246 fn select_matches(
1247 &mut self,
1248 matches: &[Self::Match],
1249 window: &mut Window,
1250 cx: &mut Context<Self>,
1251 ) {
1252 self.editor
1253 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1254 }
1255
1256 fn find_matches(
1257 &mut self,
1258 query: Arc<project::search::SearchQuery>,
1259 window: &mut Window,
1260 cx: &mut Context<Self>,
1261 ) -> gpui::Task<Vec<Self::Match>> {
1262 self.editor
1263 .update(cx, |e, cx| e.find_matches(query, window, cx))
1264 }
1265
1266 fn replace(
1267 &mut self,
1268 _: &Self::Match,
1269 _: &SearchQuery,
1270 _window: &mut Window,
1271 _: &mut Context<Self>,
1272 ) {
1273 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1274 }
1275 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1276 workspace::searchable::SearchOptions {
1277 case: true,
1278 word: true,
1279 regex: true,
1280 find_in_results: false,
1281 // LSP log is read-only.
1282 replacement: false,
1283 selection: false,
1284 }
1285 }
1286 fn active_match_index(
1287 &mut self,
1288 direction: Direction,
1289 matches: &[Self::Match],
1290 window: &mut Window,
1291 cx: &mut Context<Self>,
1292 ) -> Option<usize> {
1293 self.editor.update(cx, |e, cx| {
1294 e.active_match_index(direction, matches, window, cx)
1295 })
1296 }
1297}
1298
1299impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1300
1301impl ToolbarItemView for LspLogToolbarItemView {
1302 fn set_active_pane_item(
1303 &mut self,
1304 active_pane_item: Option<&dyn ItemHandle>,
1305 _: &mut Window,
1306 cx: &mut Context<Self>,
1307 ) -> workspace::ToolbarItemLocation {
1308 if let Some(item) = active_pane_item
1309 && let Some(log_view) = item.downcast::<LspLogView>()
1310 {
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 {}