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 if 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
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 if let Some(server_id) = first_server_id_for_project {
738 match this.active_entry_kind {
739 LogKind::Rpc => {
740 this.show_rpc_trace_for_server(server_id, window, cx)
741 }
742 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
743 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
744 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
745 }
746 }
747 }
748 } else if let Some(server_id) = first_server_id_for_project {
749 match this.active_entry_kind {
750 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
751 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
752 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
753 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
754 }
755 }
756
757 cx.notify();
758 });
759 let events_subscriptions = cx.subscribe_in(
760 &log_store,
761 window,
762 move |log_view, _, e, window, cx| match e {
763 Event::NewServerLogEntry { id, kind, text } => {
764 if log_view.current_server_id == Some(*id)
765 && *kind == log_view.active_entry_kind
766 {
767 log_view.editor.update(cx, |editor, cx| {
768 editor.set_read_only(false);
769 let last_offset = editor.buffer().read(cx).len(cx);
770 let newest_cursor_is_at_end =
771 editor.selections.newest::<usize>(cx).start >= last_offset;
772 editor.edit(
773 vec![
774 (last_offset..last_offset, text.as_str()),
775 (last_offset..last_offset, "\n"),
776 ],
777 cx,
778 );
779 if text.len() > 1024 {
780 if let Some((fold_offset, _)) =
781 text.char_indices().dropping(1024).next()
782 {
783 if fold_offset < text.len() {
784 editor.fold_ranges(
785 vec![
786 last_offset + fold_offset..last_offset + text.len(),
787 ],
788 false,
789 window,
790 cx,
791 );
792 }
793 }
794 }
795
796 if newest_cursor_is_at_end {
797 editor.request_autoscroll(Autoscroll::bottom(), cx);
798 }
799 editor.set_read_only(true);
800 });
801 }
802 }
803 },
804 );
805 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
806
807 let focus_handle = cx.focus_handle();
808 let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
809 window.focus(&log_view.editor.focus_handle(cx));
810 });
811
812 let mut this = Self {
813 focus_handle,
814 editor,
815 editor_subscriptions,
816 project,
817 log_store,
818 current_server_id: None,
819 active_entry_kind: LogKind::Logs,
820 _log_store_subscriptions: vec![
821 model_changes_subscription,
822 events_subscriptions,
823 focus_subscription,
824 ],
825 };
826 if let Some(server_id) = server_id {
827 this.show_logs_for_server(server_id, window, cx);
828 }
829 this
830 }
831
832 fn editor_for_logs(
833 log_contents: String,
834 window: &mut Window,
835 cx: &mut Context<Self>,
836 ) -> (Entity<Editor>, Vec<Subscription>) {
837 let editor = initialize_new_editor(log_contents, true, window, cx);
838 let editor_subscription = cx.subscribe(
839 &editor,
840 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
841 );
842 let search_subscription = cx.subscribe(
843 &editor,
844 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
845 );
846 (editor, vec![editor_subscription, search_subscription])
847 }
848
849 fn editor_for_server_info(
850 server: &LanguageServer,
851 window: &mut Window,
852 cx: &mut Context<Self>,
853 ) -> (Entity<Editor>, Vec<Subscription>) {
854 let server_info = format!(
855 "* Server: {NAME} (id {ID})
856
857* Binary: {BINARY:#?}
858
859* Registered workspace folders:
860{WORKSPACE_FOLDERS}
861
862* Capabilities: {CAPABILITIES}
863
864* Configuration: {CONFIGURATION}",
865 NAME = server.name(),
866 ID = server.server_id(),
867 BINARY = server.binary(),
868 WORKSPACE_FOLDERS = server
869 .workspace_folders()
870 .into_iter()
871 .filter_map(|path| path
872 .to_file_path()
873 .ok()
874 .map(|path| path.to_string_lossy().into_owned()))
875 .collect::<Vec<_>>()
876 .join(", "),
877 CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
878 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
879 CONFIGURATION = serde_json::to_string_pretty(server.configuration())
880 .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
881 );
882 let editor = initialize_new_editor(server_info, false, window, cx);
883 let editor_subscription = cx.subscribe(
884 &editor,
885 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
886 );
887 let search_subscription = cx.subscribe(
888 &editor,
889 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
890 );
891 (editor, vec![editor_subscription, search_subscription])
892 }
893
894 pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
895 let log_store = self.log_store.read(cx);
896
897 let unknown_server = LanguageServerName::new_static("unknown server");
898
899 let mut rows = log_store
900 .language_servers
901 .iter()
902 .map(|(server_id, state)| match &state.kind {
903 LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
904 let worktree_root_name = state
905 .worktree_id
906 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
907 .map(|worktree| worktree.read(cx).root_name().to_string())
908 .unwrap_or_else(|| "Unknown worktree".to_string());
909
910 LogMenuItem {
911 server_id: *server_id,
912 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
913 server_kind: state.kind.clone(),
914 worktree_root_name,
915 rpc_trace_enabled: state.rpc_state.is_some(),
916 selected_entry: self.active_entry_kind,
917 trace_level: lsp::TraceValue::Off,
918 }
919 }
920
921 LanguageServerKind::Global => LogMenuItem {
922 server_id: *server_id,
923 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
924 server_kind: state.kind.clone(),
925 worktree_root_name: "supplementary".to_string(),
926 rpc_trace_enabled: state.rpc_state.is_some(),
927 selected_entry: self.active_entry_kind,
928 trace_level: lsp::TraceValue::Off,
929 },
930 })
931 .chain(
932 self.project
933 .read(cx)
934 .supplementary_language_servers(cx)
935 .filter_map(|(server_id, name)| {
936 let state = log_store.language_servers.get(&server_id)?;
937 Some(LogMenuItem {
938 server_id,
939 server_name: name.clone(),
940 server_kind: state.kind.clone(),
941 worktree_root_name: "supplementary".to_string(),
942 rpc_trace_enabled: state.rpc_state.is_some(),
943 selected_entry: self.active_entry_kind,
944 trace_level: lsp::TraceValue::Off,
945 })
946 }),
947 )
948 .collect::<Vec<_>>();
949 rows.sort_by_key(|row| row.server_id);
950 rows.dedup_by_key(|row| row.server_id);
951 Some(rows)
952 }
953
954 fn show_logs_for_server(
955 &mut self,
956 server_id: LanguageServerId,
957 window: &mut Window,
958 cx: &mut Context<Self>,
959 ) {
960 let typ = self
961 .log_store
962 .read(cx)
963 .language_servers
964 .get(&server_id)
965 .map(|v| v.log_level)
966 .unwrap_or(MessageType::LOG);
967 let log_contents = self
968 .log_store
969 .read(cx)
970 .server_logs(server_id)
971 .map(|v| log_contents(v, typ));
972 if let Some(log_contents) = log_contents {
973 self.current_server_id = Some(server_id);
974 self.active_entry_kind = LogKind::Logs;
975 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
976 self.editor = editor;
977 self.editor_subscriptions = editor_subscriptions;
978 cx.notify();
979 }
980 self.editor.read(cx).focus_handle(cx).focus(window);
981 }
982
983 fn update_log_level(
984 &self,
985 server_id: LanguageServerId,
986 level: MessageType,
987 window: &mut Window,
988 cx: &mut Context<Self>,
989 ) {
990 let log_contents = self.log_store.update(cx, |this, _| {
991 if let Some(state) = this.get_language_server_state(server_id) {
992 state.log_level = level;
993 }
994
995 this.server_logs(server_id).map(|v| log_contents(v, level))
996 });
997
998 if let Some(log_contents) = log_contents {
999 self.editor.update(cx, |editor, cx| {
1000 editor.set_text(log_contents, window, cx);
1001 editor.move_to_end(&MoveToEnd, window, cx);
1002 });
1003 cx.notify();
1004 }
1005
1006 self.editor.read(cx).focus_handle(cx).focus(window);
1007 }
1008
1009 fn show_trace_for_server(
1010 &mut self,
1011 server_id: LanguageServerId,
1012 window: &mut Window,
1013 cx: &mut Context<Self>,
1014 ) {
1015 let log_contents = self
1016 .log_store
1017 .read(cx)
1018 .server_trace(server_id)
1019 .map(|v| log_contents(v, ()));
1020 if let Some(log_contents) = log_contents {
1021 self.current_server_id = Some(server_id);
1022 self.active_entry_kind = LogKind::Trace;
1023 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
1024 self.editor = editor;
1025 self.editor_subscriptions = editor_subscriptions;
1026 cx.notify();
1027 }
1028 self.editor.read(cx).focus_handle(cx).focus(window);
1029 }
1030
1031 fn show_rpc_trace_for_server(
1032 &mut self,
1033 server_id: LanguageServerId,
1034 window: &mut Window,
1035 cx: &mut Context<Self>,
1036 ) {
1037 let rpc_log = self.log_store.update(cx, |log_store, _| {
1038 log_store
1039 .enable_rpc_trace_for_language_server(server_id)
1040 .map(|state| log_contents(&state.rpc_messages, ()))
1041 });
1042 if let Some(rpc_log) = rpc_log {
1043 self.current_server_id = Some(server_id);
1044 self.active_entry_kind = LogKind::Rpc;
1045 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
1046 let language = self.project.read(cx).languages().language_for_name("JSON");
1047 editor
1048 .read(cx)
1049 .buffer()
1050 .read(cx)
1051 .as_singleton()
1052 .expect("log buffer should be a singleton")
1053 .update(cx, |_, cx| {
1054 cx.spawn({
1055 let buffer = cx.entity();
1056 async move |_, cx| {
1057 let language = language.await.ok();
1058 buffer.update(cx, |buffer, cx| {
1059 buffer.set_language(language, cx);
1060 })
1061 }
1062 })
1063 .detach_and_log_err(cx);
1064 });
1065
1066 self.editor = editor;
1067 self.editor_subscriptions = editor_subscriptions;
1068 cx.notify();
1069 }
1070
1071 self.editor.read(cx).focus_handle(cx).focus(window);
1072 }
1073
1074 fn toggle_rpc_trace_for_server(
1075 &mut self,
1076 server_id: LanguageServerId,
1077 enabled: bool,
1078 window: &mut Window,
1079 cx: &mut Context<Self>,
1080 ) {
1081 self.log_store.update(cx, |log_store, _| {
1082 if enabled {
1083 log_store.enable_rpc_trace_for_language_server(server_id);
1084 } else {
1085 log_store.disable_rpc_trace_for_language_server(server_id);
1086 }
1087 });
1088 if !enabled && Some(server_id) == self.current_server_id {
1089 self.show_logs_for_server(server_id, window, cx);
1090 cx.notify();
1091 }
1092 }
1093
1094 fn update_trace_level(
1095 &self,
1096 server_id: LanguageServerId,
1097 level: TraceValue,
1098 cx: &mut Context<Self>,
1099 ) {
1100 if let Some(server) = self
1101 .project
1102 .read(cx)
1103 .lsp_store()
1104 .read(cx)
1105 .language_server_for_id(server_id)
1106 {
1107 self.log_store.update(cx, |this, _| {
1108 if let Some(state) = this.get_language_server_state(server_id) {
1109 state.trace_level = level;
1110 }
1111 });
1112
1113 server
1114 .notify::<SetTrace>(&SetTraceParams { value: level })
1115 .ok();
1116 }
1117 }
1118
1119 fn show_server_info(
1120 &mut self,
1121 server_id: LanguageServerId,
1122 window: &mut Window,
1123 cx: &mut Context<Self>,
1124 ) {
1125 let lsp_store = self.project.read(cx).lsp_store();
1126 let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
1127 return;
1128 };
1129 self.current_server_id = Some(server_id);
1130 self.active_entry_kind = LogKind::ServerInfo;
1131 let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx);
1132 self.editor = editor;
1133 self.editor_subscriptions = editor_subscriptions;
1134 cx.notify();
1135 self.editor.read(cx).focus_handle(cx).focus(window);
1136 }
1137}
1138
1139fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
1140 lines
1141 .iter()
1142 .filter(|message| message.should_include(level))
1143 .flat_map(|message| [message.as_ref(), "\n"])
1144 .collect()
1145}
1146
1147impl Render for LspLogView {
1148 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1149 self.editor.update(cx, |editor, cx| {
1150 editor.render(window, cx).into_any_element()
1151 })
1152 }
1153}
1154
1155impl Focusable for LspLogView {
1156 fn focus_handle(&self, _: &App) -> FocusHandle {
1157 self.focus_handle.clone()
1158 }
1159}
1160
1161impl Item for LspLogView {
1162 type Event = EditorEvent;
1163
1164 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1165 Editor::to_item_events(event, f)
1166 }
1167
1168 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1169 "LSP Logs".into()
1170 }
1171
1172 fn telemetry_event_text(&self) -> Option<&'static str> {
1173 None
1174 }
1175
1176 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1177 Some(Box::new(handle.clone()))
1178 }
1179
1180 fn act_as_type<'a>(
1181 &'a self,
1182 type_id: TypeId,
1183 self_handle: &'a Entity<Self>,
1184 _: &'a App,
1185 ) -> Option<AnyView> {
1186 if type_id == TypeId::of::<Self>() {
1187 Some(self_handle.to_any())
1188 } else if type_id == TypeId::of::<Editor>() {
1189 Some(self.editor.to_any())
1190 } else {
1191 None
1192 }
1193 }
1194
1195 fn clone_on_split(
1196 &self,
1197 _workspace_id: Option<WorkspaceId>,
1198 window: &mut Window,
1199 cx: &mut Context<Self>,
1200 ) -> Option<Entity<Self>>
1201 where
1202 Self: Sized,
1203 {
1204 Some(cx.new(|cx| {
1205 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
1206 if let Some(server_id) = self.current_server_id {
1207 match self.active_entry_kind {
1208 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
1209 LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
1210 LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
1211 LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
1212 }
1213 }
1214 new_view
1215 }))
1216 }
1217}
1218
1219impl SearchableItem for LspLogView {
1220 type Match = <Editor as SearchableItem>::Match;
1221
1222 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1223 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1224 }
1225
1226 fn update_matches(
1227 &mut self,
1228 matches: &[Self::Match],
1229 window: &mut Window,
1230 cx: &mut Context<Self>,
1231 ) {
1232 self.editor
1233 .update(cx, |e, cx| e.update_matches(matches, window, cx))
1234 }
1235
1236 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1237 self.editor
1238 .update(cx, |e, cx| e.query_suggestion(window, cx))
1239 }
1240
1241 fn activate_match(
1242 &mut self,
1243 index: usize,
1244 matches: &[Self::Match],
1245 window: &mut Window,
1246 cx: &mut Context<Self>,
1247 ) {
1248 self.editor
1249 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1250 }
1251
1252 fn select_matches(
1253 &mut self,
1254 matches: &[Self::Match],
1255 window: &mut Window,
1256 cx: &mut Context<Self>,
1257 ) {
1258 self.editor
1259 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1260 }
1261
1262 fn find_matches(
1263 &mut self,
1264 query: Arc<project::search::SearchQuery>,
1265 window: &mut Window,
1266 cx: &mut Context<Self>,
1267 ) -> gpui::Task<Vec<Self::Match>> {
1268 self.editor
1269 .update(cx, |e, cx| e.find_matches(query, window, cx))
1270 }
1271
1272 fn replace(
1273 &mut self,
1274 _: &Self::Match,
1275 _: &SearchQuery,
1276 _window: &mut Window,
1277 _: &mut Context<Self>,
1278 ) {
1279 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1280 }
1281 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1282 workspace::searchable::SearchOptions {
1283 case: true,
1284 word: true,
1285 regex: true,
1286 find_in_results: false,
1287 // LSP log is read-only.
1288 replacement: false,
1289 selection: false,
1290 }
1291 }
1292 fn active_match_index(
1293 &mut self,
1294 direction: Direction,
1295 matches: &[Self::Match],
1296 window: &mut Window,
1297 cx: &mut Context<Self>,
1298 ) -> Option<usize> {
1299 self.editor.update(cx, |e, cx| {
1300 e.active_match_index(direction, matches, window, cx)
1301 })
1302 }
1303}
1304
1305impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1306
1307impl ToolbarItemView for LspLogToolbarItemView {
1308 fn set_active_pane_item(
1309 &mut self,
1310 active_pane_item: Option<&dyn ItemHandle>,
1311 _: &mut Window,
1312 cx: &mut Context<Self>,
1313 ) -> workspace::ToolbarItemLocation {
1314 if let Some(item) = active_pane_item {
1315 if let Some(log_view) = item.downcast::<LspLogView>() {
1316 self.log_view = Some(log_view.clone());
1317 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1318 cx.notify();
1319 }));
1320 return ToolbarItemLocation::PrimaryLeft;
1321 }
1322 }
1323 self.log_view = None;
1324 self._log_view_subscription = None;
1325 ToolbarItemLocation::Hidden
1326 }
1327}
1328
1329impl Render for LspLogToolbarItemView {
1330 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1331 let Some(log_view) = self.log_view.clone() else {
1332 return div();
1333 };
1334
1335 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1336 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1337 let current_server_id = log_view.current_server_id;
1338 (menu_rows, current_server_id)
1339 });
1340
1341 let current_server = current_server_id.and_then(|current_server_id| {
1342 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1343 Some(menu_rows[ix].clone())
1344 } else {
1345 None
1346 }
1347 });
1348
1349 let available_language_servers: Vec<_> = menu_rows
1350 .into_iter()
1351 .map(|row| {
1352 (
1353 row.server_id,
1354 row.server_name,
1355 row.worktree_root_name,
1356 row.selected_entry,
1357 )
1358 })
1359 .collect();
1360
1361 let log_toolbar_view = cx.entity();
1362
1363 let lsp_menu = PopoverMenu::new("LspLogView")
1364 .anchor(Corner::TopLeft)
1365 .trigger(
1366 Button::new(
1367 "language_server_menu_header",
1368 current_server
1369 .as_ref()
1370 .map(|row| {
1371 Cow::Owned(format!(
1372 "{} ({})",
1373 row.server_name.0, row.worktree_root_name,
1374 ))
1375 })
1376 .unwrap_or_else(|| "No server selected".into()),
1377 )
1378 .icon(IconName::ChevronDown)
1379 .icon_size(IconSize::Small)
1380 .icon_color(Color::Muted),
1381 )
1382 .menu({
1383 let log_view = log_view.clone();
1384 move |window, cx| {
1385 let log_view = log_view.clone();
1386 ContextMenu::build(window, cx, |mut menu, window, _| {
1387 for (server_id, name, worktree_root, active_entry_kind) in
1388 available_language_servers.iter()
1389 {
1390 let label = format!("{} ({})", name, worktree_root);
1391 let server_id = *server_id;
1392 let active_entry_kind = *active_entry_kind;
1393 menu = menu.entry(
1394 label,
1395 None,
1396 window.handler_for(&log_view, move |view, window, cx| {
1397 view.current_server_id = Some(server_id);
1398 view.active_entry_kind = active_entry_kind;
1399 match view.active_entry_kind {
1400 LogKind::Rpc => {
1401 view.toggle_rpc_trace_for_server(
1402 server_id, true, window, cx,
1403 );
1404 view.show_rpc_trace_for_server(server_id, window, cx);
1405 }
1406 LogKind::Trace => {
1407 view.show_trace_for_server(server_id, window, cx)
1408 }
1409 LogKind::Logs => {
1410 view.show_logs_for_server(server_id, window, cx)
1411 }
1412 LogKind::ServerInfo => {
1413 view.show_server_info(server_id, window, cx)
1414 }
1415 }
1416 cx.notify();
1417 }),
1418 );
1419 }
1420 menu
1421 })
1422 .into()
1423 }
1424 });
1425
1426 let view_selector = current_server.map(|server| {
1427 let server_id = server.server_id;
1428 let is_remote = server.server_kind.is_remote();
1429 let rpc_trace_enabled = server.rpc_trace_enabled;
1430 let log_view = log_view.clone();
1431 PopoverMenu::new("LspViewSelector")
1432 .anchor(Corner::TopLeft)
1433 .trigger(
1434 Button::new("language_server_menu_header", server.selected_entry.label())
1435 .icon(IconName::ChevronDown)
1436 .icon_size(IconSize::Small)
1437 .icon_color(Color::Muted),
1438 )
1439 .menu(move |window, cx| {
1440 let log_toolbar_view = log_toolbar_view.clone();
1441 let log_view = log_view.clone();
1442 Some(ContextMenu::build(window, cx, move |this, window, _| {
1443 this.entry(
1444 SERVER_LOGS,
1445 None,
1446 window.handler_for(&log_view, move |view, window, cx| {
1447 view.show_logs_for_server(server_id, window, cx);
1448 }),
1449 )
1450 .when(!is_remote, |this| {
1451 this.entry(
1452 SERVER_TRACE,
1453 None,
1454 window.handler_for(&log_view, move |view, window, cx| {
1455 view.show_trace_for_server(server_id, window, cx);
1456 }),
1457 )
1458 .custom_entry(
1459 {
1460 let log_toolbar_view = log_toolbar_view.clone();
1461 move |window, _| {
1462 h_flex()
1463 .w_full()
1464 .justify_between()
1465 .child(Label::new(RPC_MESSAGES))
1466 .child(
1467 div().child(
1468 Checkbox::new(
1469 "LspLogEnableRpcTrace",
1470 if rpc_trace_enabled {
1471 ToggleState::Selected
1472 } else {
1473 ToggleState::Unselected
1474 },
1475 )
1476 .on_click(window.listener_for(
1477 &log_toolbar_view,
1478 move |view, selection, window, cx| {
1479 let enabled = matches!(
1480 selection,
1481 ToggleState::Selected
1482 );
1483 view.toggle_rpc_logging_for_server(
1484 server_id, enabled, window, cx,
1485 );
1486 cx.stop_propagation();
1487 },
1488 )),
1489 ),
1490 )
1491 .into_any_element()
1492 }
1493 },
1494 window.handler_for(&log_view, move |view, window, cx| {
1495 view.show_rpc_trace_for_server(server_id, window, cx);
1496 }),
1497 )
1498 })
1499 .entry(
1500 SERVER_INFO,
1501 None,
1502 window.handler_for(&log_view, move |view, window, cx| {
1503 view.show_server_info(server_id, window, cx);
1504 }),
1505 )
1506 }))
1507 })
1508 });
1509
1510 h_flex()
1511 .size_full()
1512 .gap_1()
1513 .justify_between()
1514 .child(
1515 h_flex()
1516 .gap_0p5()
1517 .child(lsp_menu)
1518 .children(view_selector)
1519 .child(
1520 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1521 LogKind::Trace => {
1522 let log_view = log_view.clone();
1523 div().child(
1524 PopoverMenu::new("lsp-trace-level-menu")
1525 .anchor(Corner::TopLeft)
1526 .trigger(
1527 Button::new(
1528 "language_server_trace_level_selector",
1529 "Trace level",
1530 )
1531 .icon(IconName::ChevronDown)
1532 .icon_size(IconSize::Small)
1533 .icon_color(Color::Muted),
1534 )
1535 .menu({
1536 let log_view = log_view.clone();
1537
1538 move |window, cx| {
1539 let id = log_view.read(cx).current_server_id?;
1540
1541 let trace_level =
1542 log_view.update(cx, |this, cx| {
1543 this.log_store.update(cx, |this, _| {
1544 Some(
1545 this.get_language_server_state(id)?
1546 .trace_level,
1547 )
1548 })
1549 })?;
1550
1551 ContextMenu::build(
1552 window,
1553 cx,
1554 |mut menu, window, cx| {
1555 let log_view = log_view.clone();
1556
1557 for (option, label) in [
1558 (TraceValue::Off, "Off"),
1559 (TraceValue::Messages, "Messages"),
1560 (TraceValue::Verbose, "Verbose"),
1561 ] {
1562 menu = menu.entry(label, None, {
1563 let log_view = log_view.clone();
1564 move |_, cx| {
1565 log_view.update(cx, |this, cx| {
1566 if let Some(id) =
1567 this.current_server_id
1568 {
1569 this.update_trace_level(
1570 id, option, cx,
1571 );
1572 }
1573 });
1574 }
1575 });
1576 if option == trace_level {
1577 menu.select_last(window, cx);
1578 }
1579 }
1580
1581 menu
1582 },
1583 )
1584 .into()
1585 }
1586 }),
1587 )
1588 }
1589 LogKind::Logs => {
1590 let log_view = log_view.clone();
1591 div().child(
1592 PopoverMenu::new("lsp-log-level-menu")
1593 .anchor(Corner::TopLeft)
1594 .trigger(
1595 Button::new(
1596 "language_server_log_level_selector",
1597 "Log level",
1598 )
1599 .icon(IconName::ChevronDown)
1600 .icon_size(IconSize::Small)
1601 .icon_color(Color::Muted),
1602 )
1603 .menu({
1604 let log_view = log_view.clone();
1605
1606 move |window, cx| {
1607 let id = log_view.read(cx).current_server_id?;
1608
1609 let log_level =
1610 log_view.update(cx, |this, cx| {
1611 this.log_store.update(cx, |this, _| {
1612 Some(
1613 this.get_language_server_state(id)?
1614 .log_level,
1615 )
1616 })
1617 })?;
1618
1619 ContextMenu::build(
1620 window,
1621 cx,
1622 |mut menu, window, cx| {
1623 let log_view = log_view.clone();
1624
1625 for (option, label) in [
1626 (MessageType::LOG, "Log"),
1627 (MessageType::INFO, "Info"),
1628 (MessageType::WARNING, "Warning"),
1629 (MessageType::ERROR, "Error"),
1630 ] {
1631 menu = menu.entry(label, None, {
1632 let log_view = log_view.clone();
1633 move |window, cx| {
1634 log_view.update(cx, |this, cx| {
1635 if let Some(id) =
1636 this.current_server_id
1637 {
1638 this.update_log_level(
1639 id, option, window, cx,
1640 );
1641 }
1642 });
1643 }
1644 });
1645 if option == log_level {
1646 menu.select_last(window, cx);
1647 }
1648 }
1649
1650 menu
1651 },
1652 )
1653 .into()
1654 }
1655 }),
1656 )
1657 }
1658 _ => div(),
1659 }),
1660 ),
1661 )
1662 .child(
1663 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1664 |this, _, window, cx| {
1665 if let Some(log_view) = this.log_view.as_ref() {
1666 log_view.update(cx, |log_view, cx| {
1667 log_view.editor.update(cx, |editor, cx| {
1668 editor.set_read_only(false);
1669 editor.clear(window, cx);
1670 editor.set_read_only(true);
1671 });
1672 })
1673 }
1674 },
1675 )),
1676 )
1677 }
1678}
1679
1680fn initialize_new_editor(
1681 content: String,
1682 move_to_end: bool,
1683 window: &mut Window,
1684 cx: &mut App,
1685) -> Entity<Editor> {
1686 cx.new(|cx| {
1687 let mut editor = Editor::multi_line(window, cx);
1688 editor.hide_minimap_by_default(window, cx);
1689 editor.set_text(content, window, cx);
1690 editor.set_show_git_diff_gutter(false, cx);
1691 editor.set_show_runnables(false, cx);
1692 editor.set_show_breakpoints(false, cx);
1693 editor.set_read_only(true);
1694 editor.set_show_edit_predictions(Some(false), window, cx);
1695 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1696 if move_to_end {
1697 editor.move_to_end(&MoveToEnd, window, cx);
1698 }
1699 editor
1700 })
1701}
1702
1703const RPC_MESSAGES: &str = "RPC Messages";
1704const SERVER_LOGS: &str = "Server Logs";
1705const SERVER_TRACE: &str = "Server Trace";
1706const SERVER_INFO: &str = "Server Info";
1707
1708impl Default for LspLogToolbarItemView {
1709 fn default() -> Self {
1710 Self::new()
1711 }
1712}
1713
1714impl LspLogToolbarItemView {
1715 pub fn new() -> Self {
1716 Self {
1717 log_view: None,
1718 _log_view_subscription: None,
1719 }
1720 }
1721
1722 fn toggle_rpc_logging_for_server(
1723 &mut self,
1724 id: LanguageServerId,
1725 enabled: bool,
1726 window: &mut Window,
1727 cx: &mut Context<Self>,
1728 ) {
1729 if let Some(log_view) = &self.log_view {
1730 log_view.update(cx, |log_view, cx| {
1731 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1732 if !enabled && Some(id) == log_view.current_server_id {
1733 log_view.show_logs_for_server(id, window, cx);
1734 cx.notify();
1735 } else if enabled {
1736 log_view.show_rpc_trace_for_server(id, window, cx);
1737 cx.notify();
1738 }
1739 window.focus(&log_view.focus_handle);
1740 });
1741 }
1742 cx.notify();
1743 }
1744}
1745
1746pub enum Event {
1747 NewServerLogEntry {
1748 id: LanguageServerId,
1749 kind: LogKind,
1750 text: String,
1751 },
1752}
1753
1754impl EventEmitter<Event> for LogStore {}
1755impl EventEmitter<Event> for LspLogView {}
1756impl EventEmitter<EditorEvent> for LspLogView {}
1757impl EventEmitter<SearchEvent> for LspLogView {}