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!(dev, [OpenLanguageServerLogs]);
208
209pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
210
211impl Global for GlobalLogStore {}
212
213pub fn init(cx: &mut App) {
214 let log_store = cx.new(LogStore::new);
215 cx.set_global(GlobalLogStore(log_store.downgrade()));
216
217 cx.observe_new(move |workspace: &mut Workspace, _, cx| {
218 let project = workspace.project();
219 if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
220 log_store.update(cx, |store, cx| {
221 store.add_project(project, cx);
222 });
223 }
224
225 let log_store = log_store.clone();
226 workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
227 let project = workspace.project().read(cx);
228 if project.is_local() || project.is_via_ssh() {
229 let project = workspace.project().clone();
230 let log_store = log_store.clone();
231 get_or_create_tool(
232 workspace,
233 SplitDirection::Right,
234 window,
235 cx,
236 move |window, cx| LspLogView::new(project, log_store, window, cx),
237 );
238 }
239 });
240 })
241 .detach();
242}
243
244impl LogStore {
245 pub fn new(cx: &mut Context<Self>) -> Self {
246 let (io_tx, mut io_rx) = mpsc::unbounded();
247
248 let copilot_subscription = Copilot::global(cx).map(|copilot| {
249 let copilot = &copilot;
250 cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| {
251 if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event {
252 if let Some(server) = copilot.read(cx).language_server() {
253 let server_id = server.server_id();
254 let weak_this = cx.weak_entity();
255 this.copilot_log_subscription =
256 Some(server.on_notification::<copilot::request::LogMessage, _>(
257 move |params, cx| {
258 weak_this
259 .update(cx, |this, cx| {
260 this.add_language_server_log(
261 server_id,
262 MessageType::LOG,
263 ¶ms.message,
264 cx,
265 );
266 })
267 .ok();
268 },
269 ));
270 let name = LanguageServerName::new_static("copilot");
271 this.add_language_server(
272 LanguageServerKind::Global,
273 server.server_id(),
274 Some(name),
275 None,
276 Some(server.clone()),
277 cx,
278 );
279 }
280 }
281 })
282 });
283
284 let this = Self {
285 copilot_log_subscription: None,
286 _copilot_subscription: copilot_subscription,
287 projects: HashMap::default(),
288 language_servers: HashMap::default(),
289 io_tx,
290 };
291
292 cx.spawn(async move |this, cx| {
293 while let Some((server_id, io_kind, message)) = io_rx.next().await {
294 if let Some(this) = this.upgrade() {
295 this.update(cx, |this, cx| {
296 this.on_io(server_id, io_kind, &message, cx);
297 })?;
298 }
299 }
300 anyhow::Ok(())
301 })
302 .detach_and_log_err(cx);
303 this
304 }
305
306 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
307 let weak_project = project.downgrade();
308 self.projects.insert(
309 project.downgrade(),
310 ProjectState {
311 _subscriptions: [
312 cx.observe_release(project, move |this, _, _| {
313 this.projects.remove(&weak_project);
314 this.language_servers
315 .retain(|_, state| state.kind.project() != Some(&weak_project));
316 }),
317 cx.subscribe(project, |this, project, event, cx| {
318 let server_kind = if project.read(cx).is_via_ssh() {
319 LanguageServerKind::Remote {
320 project: project.downgrade(),
321 }
322 } else {
323 LanguageServerKind::Local {
324 project: project.downgrade(),
325 }
326 };
327
328 match event {
329 project::Event::LanguageServerAdded(id, name, worktree_id) => {
330 this.add_language_server(
331 server_kind,
332 *id,
333 Some(name.clone()),
334 *worktree_id,
335 project
336 .read(cx)
337 .lsp_store()
338 .read(cx)
339 .language_server_for_id(*id),
340 cx,
341 );
342 }
343 project::Event::LanguageServerRemoved(id) => {
344 this.remove_language_server(*id, cx);
345 }
346 project::Event::LanguageServerLog(id, typ, message) => {
347 this.add_language_server(server_kind, *id, None, None, None, cx);
348 match typ {
349 project::LanguageServerLogType::Log(typ) => {
350 this.add_language_server_log(*id, *typ, message, cx);
351 }
352 project::LanguageServerLogType::Trace(_) => {
353 this.add_language_server_trace(*id, message, cx);
354 }
355 }
356 }
357 _ => {}
358 }
359 }),
360 ],
361 },
362 );
363 }
364
365 pub(super) fn get_language_server_state(
366 &mut self,
367 id: LanguageServerId,
368 ) -> Option<&mut LanguageServerState> {
369 self.language_servers.get_mut(&id)
370 }
371
372 fn add_language_server(
373 &mut self,
374 kind: LanguageServerKind,
375 server_id: LanguageServerId,
376 name: Option<LanguageServerName>,
377 worktree_id: Option<WorktreeId>,
378 server: Option<Arc<LanguageServer>>,
379 cx: &mut Context<Self>,
380 ) -> Option<&mut LanguageServerState> {
381 let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
382 cx.notify();
383 LanguageServerState {
384 name: None,
385 worktree_id: None,
386 kind,
387 rpc_state: None,
388 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
389 trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
390 trace_level: TraceValue::Off,
391 log_level: MessageType::LOG,
392 io_logs_subscription: None,
393 }
394 });
395
396 if let Some(name) = name {
397 server_state.name = Some(name);
398 }
399 if let Some(worktree_id) = worktree_id {
400 server_state.worktree_id = Some(worktree_id);
401 }
402
403 if let Some(server) = server
404 .clone()
405 .filter(|_| server_state.io_logs_subscription.is_none())
406 {
407 let io_tx = self.io_tx.clone();
408 let server_id = server.server_id();
409 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
410 io_tx
411 .unbounded_send((server_id, io_kind, message.to_string()))
412 .ok();
413 }));
414 }
415
416 Some(server_state)
417 }
418
419 fn add_language_server_log(
420 &mut self,
421 id: LanguageServerId,
422 typ: MessageType,
423 message: &str,
424 cx: &mut Context<Self>,
425 ) -> Option<()> {
426 let language_server_state = self.get_language_server_state(id)?;
427
428 let log_lines = &mut language_server_state.log_messages;
429 Self::add_language_server_message(
430 log_lines,
431 id,
432 LogMessage {
433 message: message.trim_end().to_string(),
434 typ,
435 },
436 language_server_state.log_level,
437 LogKind::Logs,
438 cx,
439 );
440 Some(())
441 }
442
443 fn add_language_server_trace(
444 &mut self,
445 id: LanguageServerId,
446 message: &str,
447 cx: &mut Context<Self>,
448 ) -> Option<()> {
449 let language_server_state = self.get_language_server_state(id)?;
450
451 let log_lines = &mut language_server_state.trace_messages;
452 Self::add_language_server_message(
453 log_lines,
454 id,
455 TraceMessage {
456 message: message.trim().to_string(),
457 },
458 (),
459 LogKind::Trace,
460 cx,
461 );
462 Some(())
463 }
464
465 fn add_language_server_message<T: Message>(
466 log_lines: &mut VecDeque<T>,
467 id: LanguageServerId,
468 message: T,
469 current_severity: <T as Message>::Level,
470 kind: LogKind,
471 cx: &mut Context<Self>,
472 ) {
473 while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
474 log_lines.pop_front();
475 }
476 let text = message.as_ref().to_string();
477 let visible = message.should_include(current_severity);
478 log_lines.push_back(message);
479
480 if visible {
481 cx.emit(Event::NewServerLogEntry { id, kind, text });
482 cx.notify();
483 }
484 }
485
486 fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
487 self.language_servers.remove(&id);
488 cx.notify();
489 }
490
491 pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
492 Some(&self.language_servers.get(&server_id)?.log_messages)
493 }
494
495 pub(super) fn server_trace(
496 &self,
497 server_id: LanguageServerId,
498 ) -> Option<&VecDeque<TraceMessage>> {
499 Some(&self.language_servers.get(&server_id)?.trace_messages)
500 }
501
502 fn server_ids_for_project<'a>(
503 &'a self,
504 lookup_project: &'a WeakEntity<Project>,
505 ) -> impl Iterator<Item = LanguageServerId> + 'a {
506 self.language_servers
507 .iter()
508 .filter_map(move |(id, state)| match &state.kind {
509 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
510 if project == lookup_project {
511 Some(*id)
512 } else {
513 None
514 }
515 }
516 LanguageServerKind::Global => Some(*id),
517 })
518 }
519
520 fn enable_rpc_trace_for_language_server(
521 &mut self,
522 server_id: LanguageServerId,
523 ) -> Option<&mut LanguageServerRpcState> {
524 let rpc_state = self
525 .language_servers
526 .get_mut(&server_id)?
527 .rpc_state
528 .get_or_insert_with(|| LanguageServerRpcState {
529 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
530 last_message_kind: None,
531 });
532 Some(rpc_state)
533 }
534
535 pub fn disable_rpc_trace_for_language_server(
536 &mut self,
537 server_id: LanguageServerId,
538 ) -> Option<()> {
539 self.language_servers.get_mut(&server_id)?.rpc_state.take();
540 Some(())
541 }
542
543 pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
544 match server {
545 LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
546 LanguageServerSelector::Name(name) => self
547 .language_servers
548 .iter()
549 .any(|(_, state)| state.name.as_ref() == Some(name)),
550 }
551 }
552
553 pub fn open_server_log(
554 &mut self,
555 workspace: WeakEntity<Workspace>,
556 server: LanguageServerSelector,
557 window: &mut Window,
558 cx: &mut Context<Self>,
559 ) {
560 cx.spawn_in(window, async move |log_store, cx| {
561 let Some(log_store) = log_store.upgrade() else {
562 return;
563 };
564 workspace
565 .update_in(cx, |workspace, window, cx| {
566 let project = workspace.project().clone();
567 let tool_log_store = log_store.clone();
568 let log_view = get_or_create_tool(
569 workspace,
570 SplitDirection::Right,
571 window,
572 cx,
573 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
574 );
575 log_view.update(cx, |log_view, cx| {
576 let server_id = match server {
577 LanguageServerSelector::Id(id) => Some(id),
578 LanguageServerSelector::Name(name) => {
579 log_store.read(cx).language_servers.iter().find_map(
580 |(id, state)| {
581 if state.name.as_ref() == Some(&name) {
582 Some(*id)
583 } else {
584 None
585 }
586 },
587 )
588 }
589 };
590 if let Some(server_id) = server_id {
591 log_view.show_logs_for_server(server_id, window, cx);
592 }
593 });
594 })
595 .ok();
596 })
597 .detach();
598 }
599
600 pub fn open_server_trace(
601 &mut self,
602 workspace: WeakEntity<Workspace>,
603 server: LanguageServerSelector,
604 window: &mut Window,
605 cx: &mut Context<Self>,
606 ) {
607 cx.spawn_in(window, async move |log_store, cx| {
608 let Some(log_store) = log_store.upgrade() else {
609 return;
610 };
611 workspace
612 .update_in(cx, |workspace, window, cx| {
613 let project = workspace.project().clone();
614 let tool_log_store = log_store.clone();
615 let log_view = get_or_create_tool(
616 workspace,
617 SplitDirection::Right,
618 window,
619 cx,
620 move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
621 );
622 log_view.update(cx, |log_view, cx| {
623 let server_id = match server {
624 LanguageServerSelector::Id(id) => Some(id),
625 LanguageServerSelector::Name(name) => {
626 log_store.read(cx).language_servers.iter().find_map(
627 |(id, state)| {
628 if state.name.as_ref() == Some(&name) {
629 Some(*id)
630 } else {
631 None
632 }
633 },
634 )
635 }
636 };
637 if let Some(server_id) = server_id {
638 log_view.show_rpc_trace_for_server(server_id, window, cx);
639 }
640 });
641 })
642 .ok();
643 })
644 .detach();
645 }
646
647 fn on_io(
648 &mut self,
649 language_server_id: LanguageServerId,
650 io_kind: IoKind,
651 message: &str,
652 cx: &mut Context<Self>,
653 ) -> Option<()> {
654 let is_received = match io_kind {
655 IoKind::StdOut => true,
656 IoKind::StdIn => false,
657 IoKind::StdErr => {
658 self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
659 return Some(());
660 }
661 };
662
663 let state = self
664 .get_language_server_state(language_server_id)?
665 .rpc_state
666 .as_mut()?;
667 let kind = if is_received {
668 MessageKind::Receive
669 } else {
670 MessageKind::Send
671 };
672
673 let rpc_log_lines = &mut state.rpc_messages;
674 if state.last_message_kind != Some(kind) {
675 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
676 rpc_log_lines.pop_front();
677 }
678 let line_before_message = match kind {
679 MessageKind::Send => SEND_LINE,
680 MessageKind::Receive => RECEIVE_LINE,
681 };
682 rpc_log_lines.push_back(RpcMessage {
683 message: line_before_message.to_string(),
684 });
685 cx.emit(Event::NewServerLogEntry {
686 id: language_server_id,
687 kind: LogKind::Rpc,
688 text: line_before_message.to_string(),
689 });
690 }
691
692 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
693 rpc_log_lines.pop_front();
694 }
695
696 let message = message.trim();
697 rpc_log_lines.push_back(RpcMessage {
698 message: message.to_string(),
699 });
700 cx.emit(Event::NewServerLogEntry {
701 id: language_server_id,
702 kind: LogKind::Rpc,
703 text: message.to_string(),
704 });
705 cx.notify();
706 Some(())
707 }
708}
709
710impl LspLogView {
711 pub fn new(
712 project: Entity<Project>,
713 log_store: Entity<LogStore>,
714 window: &mut Window,
715 cx: &mut Context<Self>,
716 ) -> Self {
717 let server_id = log_store
718 .read(cx)
719 .language_servers
720 .iter()
721 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
722 .map(|(id, _)| *id);
723
724 let weak_project = project.downgrade();
725 let model_changes_subscription =
726 cx.observe_in(&log_store, window, move |this, store, window, cx| {
727 let first_server_id_for_project =
728 store.read(cx).server_ids_for_project(&weak_project).next();
729 if let Some(current_lsp) = this.current_server_id {
730 if !store.read(cx).language_servers.contains_key(¤t_lsp) {
731 if let Some(server_id) = first_server_id_for_project {
732 match this.active_entry_kind {
733 LogKind::Rpc => {
734 this.show_rpc_trace_for_server(server_id, window, cx)
735 }
736 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
737 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
738 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
739 }
740 }
741 }
742 } else if let Some(server_id) = first_server_id_for_project {
743 match this.active_entry_kind {
744 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
745 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
746 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
747 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
748 }
749 }
750
751 cx.notify();
752 });
753 let events_subscriptions = cx.subscribe_in(
754 &log_store,
755 window,
756 move |log_view, _, e, window, cx| match e {
757 Event::NewServerLogEntry { id, kind, text } => {
758 if log_view.current_server_id == Some(*id)
759 && *kind == log_view.active_entry_kind
760 {
761 log_view.editor.update(cx, |editor, cx| {
762 editor.set_read_only(false);
763 let last_offset = editor.buffer().read(cx).len(cx);
764 let newest_cursor_is_at_end =
765 editor.selections.newest::<usize>(cx).start >= last_offset;
766 editor.edit(
767 vec![
768 (last_offset..last_offset, text.as_str()),
769 (last_offset..last_offset, "\n"),
770 ],
771 cx,
772 );
773 if text.len() > 1024 {
774 if let Some((fold_offset, _)) =
775 text.char_indices().dropping(1024).next()
776 {
777 if fold_offset < text.len() {
778 editor.fold_ranges(
779 vec![
780 last_offset + fold_offset..last_offset + text.len(),
781 ],
782 false,
783 window,
784 cx,
785 );
786 }
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 .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 if let Some(log_view) = item.downcast::<LspLogView>() {
1310 self.log_view = Some(log_view.clone());
1311 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1312 cx.notify();
1313 }));
1314 return ToolbarItemLocation::PrimaryLeft;
1315 }
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 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1329 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1330 let current_server_id = log_view.current_server_id;
1331 (menu_rows, current_server_id)
1332 });
1333
1334 let current_server = current_server_id.and_then(|current_server_id| {
1335 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1336 Some(menu_rows[ix].clone())
1337 } else {
1338 None
1339 }
1340 });
1341 let available_language_servers: Vec<_> = menu_rows
1342 .into_iter()
1343 .map(|row| {
1344 (
1345 row.server_id,
1346 row.server_name,
1347 row.worktree_root_name,
1348 row.selected_entry,
1349 )
1350 })
1351 .collect();
1352 let log_toolbar_view = cx.entity().clone();
1353 let lsp_menu = PopoverMenu::new("LspLogView")
1354 .anchor(Corner::TopLeft)
1355 .trigger(Button::new(
1356 "language_server_menu_header",
1357 current_server
1358 .as_ref()
1359 .map(|row| {
1360 Cow::Owned(format!(
1361 "{} ({})",
1362 row.server_name.0, row.worktree_root_name,
1363 ))
1364 })
1365 .unwrap_or_else(|| "No server selected".into()),
1366 ))
1367 .menu({
1368 let log_view = log_view.clone();
1369 move |window, cx| {
1370 let log_view = log_view.clone();
1371 ContextMenu::build(window, cx, |mut menu, window, _| {
1372 for (server_id, name, worktree_root, active_entry_kind) in
1373 available_language_servers.iter()
1374 {
1375 let label = format!("{} ({})", name, worktree_root);
1376 let server_id = *server_id;
1377 let active_entry_kind = *active_entry_kind;
1378 menu = menu.entry(
1379 label,
1380 None,
1381 window.handler_for(&log_view, move |view, window, cx| {
1382 view.current_server_id = Some(server_id);
1383 view.active_entry_kind = active_entry_kind;
1384 match view.active_entry_kind {
1385 LogKind::Rpc => {
1386 view.toggle_rpc_trace_for_server(
1387 server_id, true, window, cx,
1388 );
1389 view.show_rpc_trace_for_server(server_id, window, cx);
1390 }
1391 LogKind::Trace => {
1392 view.show_trace_for_server(server_id, window, cx)
1393 }
1394 LogKind::Logs => {
1395 view.show_logs_for_server(server_id, window, cx)
1396 }
1397 LogKind::ServerInfo => {
1398 view.show_server_info(server_id, window, cx)
1399 }
1400 }
1401 cx.notify();
1402 }),
1403 );
1404 }
1405 menu
1406 })
1407 .into()
1408 }
1409 });
1410 let view_selector = current_server.map(|server| {
1411 let server_id = server.server_id;
1412 let is_remote = server.server_kind.is_remote();
1413 let rpc_trace_enabled = server.rpc_trace_enabled;
1414 let log_view = log_view.clone();
1415 PopoverMenu::new("LspViewSelector")
1416 .anchor(Corner::TopLeft)
1417 .trigger(Button::new(
1418 "language_server_menu_header",
1419 server.selected_entry.label(),
1420 ))
1421 .menu(move |window, cx| {
1422 let log_toolbar_view = log_toolbar_view.clone();
1423 let log_view = log_view.clone();
1424 Some(ContextMenu::build(window, cx, move |this, window, _| {
1425 this.entry(
1426 SERVER_LOGS,
1427 None,
1428 window.handler_for(&log_view, move |view, window, cx| {
1429 view.show_logs_for_server(server_id, window, cx);
1430 }),
1431 )
1432 .when(!is_remote, |this| {
1433 this.entry(
1434 SERVER_TRACE,
1435 None,
1436 window.handler_for(&log_view, move |view, window, cx| {
1437 view.show_trace_for_server(server_id, window, cx);
1438 }),
1439 )
1440 .custom_entry(
1441 {
1442 let log_toolbar_view = log_toolbar_view.clone();
1443 move |window, _| {
1444 h_flex()
1445 .w_full()
1446 .justify_between()
1447 .child(Label::new(RPC_MESSAGES))
1448 .child(
1449 div().child(
1450 Checkbox::new(
1451 "LspLogEnableRpcTrace",
1452 if rpc_trace_enabled {
1453 ToggleState::Selected
1454 } else {
1455 ToggleState::Unselected
1456 },
1457 )
1458 .on_click(window.listener_for(
1459 &log_toolbar_view,
1460 move |view, selection, window, cx| {
1461 let enabled = matches!(
1462 selection,
1463 ToggleState::Selected
1464 );
1465 view.toggle_rpc_logging_for_server(
1466 server_id, enabled, window, cx,
1467 );
1468 cx.stop_propagation();
1469 },
1470 )),
1471 ),
1472 )
1473 .into_any_element()
1474 }
1475 },
1476 window.handler_for(&log_view, move |view, window, cx| {
1477 view.show_rpc_trace_for_server(server_id, window, cx);
1478 }),
1479 )
1480 })
1481 .entry(
1482 SERVER_INFO,
1483 None,
1484 window.handler_for(&log_view, move |view, window, cx| {
1485 view.show_server_info(server_id, window, cx);
1486 }),
1487 )
1488 }))
1489 })
1490 });
1491 h_flex()
1492 .size_full()
1493 .justify_between()
1494 .child(
1495 h_flex()
1496 .child(lsp_menu)
1497 .children(view_selector)
1498 .child(
1499 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1500 LogKind::Trace => {
1501 let log_view = log_view.clone();
1502 div().child(
1503 PopoverMenu::new("lsp-trace-level-menu")
1504 .anchor(Corner::TopLeft)
1505 .trigger(Button::new(
1506 "language_server_trace_level_selector",
1507 "Trace level",
1508 ))
1509 .menu({
1510 let log_view = log_view.clone();
1511
1512 move |window, cx| {
1513 let id = log_view.read(cx).current_server_id?;
1514
1515 let trace_level =
1516 log_view.update(cx, |this, cx| {
1517 this.log_store.update(cx, |this, _| {
1518 Some(
1519 this.get_language_server_state(id)?
1520 .trace_level,
1521 )
1522 })
1523 })?;
1524
1525 ContextMenu::build(
1526 window,
1527 cx,
1528 |mut menu, window, cx| {
1529 let log_view = log_view.clone();
1530
1531 for (option, label) in [
1532 (TraceValue::Off, "Off"),
1533 (TraceValue::Messages, "Messages"),
1534 (TraceValue::Verbose, "Verbose"),
1535 ] {
1536 menu = menu.entry(label, None, {
1537 let log_view = log_view.clone();
1538 move |_, cx| {
1539 log_view.update(cx, |this, cx| {
1540 if let Some(id) =
1541 this.current_server_id
1542 {
1543 this.update_trace_level(
1544 id, option, cx,
1545 );
1546 }
1547 });
1548 }
1549 });
1550 if option == trace_level {
1551 menu.select_last(window, cx);
1552 }
1553 }
1554
1555 menu
1556 },
1557 )
1558 .into()
1559 }
1560 }),
1561 )
1562 }
1563 LogKind::Logs => {
1564 let log_view = log_view.clone();
1565 div().child(
1566 PopoverMenu::new("lsp-log-level-menu")
1567 .anchor(Corner::TopLeft)
1568 .trigger(Button::new(
1569 "language_server_log_level_selector",
1570 "Log level",
1571 ))
1572 .menu({
1573 let log_view = log_view.clone();
1574
1575 move |window, cx| {
1576 let id = log_view.read(cx).current_server_id?;
1577
1578 let log_level =
1579 log_view.update(cx, |this, cx| {
1580 this.log_store.update(cx, |this, _| {
1581 Some(
1582 this.get_language_server_state(id)?
1583 .log_level,
1584 )
1585 })
1586 })?;
1587
1588 ContextMenu::build(
1589 window,
1590 cx,
1591 |mut menu, window, cx| {
1592 let log_view = log_view.clone();
1593
1594 for (option, label) in [
1595 (MessageType::LOG, "Log"),
1596 (MessageType::INFO, "Info"),
1597 (MessageType::WARNING, "Warning"),
1598 (MessageType::ERROR, "Error"),
1599 ] {
1600 menu = menu.entry(label, None, {
1601 let log_view = log_view.clone();
1602 move |window, cx| {
1603 log_view.update(cx, |this, cx| {
1604 if let Some(id) =
1605 this.current_server_id
1606 {
1607 this.update_log_level(
1608 id, option, window, cx,
1609 );
1610 }
1611 });
1612 }
1613 });
1614 if option == log_level {
1615 menu.select_last(window, cx);
1616 }
1617 }
1618
1619 menu
1620 },
1621 )
1622 .into()
1623 }
1624 }),
1625 )
1626 }
1627 _ => div(),
1628 }),
1629 ),
1630 )
1631 .child(
1632 div()
1633 .child(
1634 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1635 |this, _, window, cx| {
1636 if let Some(log_view) = this.log_view.as_ref() {
1637 log_view.update(cx, |log_view, cx| {
1638 log_view.editor.update(cx, |editor, cx| {
1639 editor.set_read_only(false);
1640 editor.clear(window, cx);
1641 editor.set_read_only(true);
1642 });
1643 })
1644 }
1645 },
1646 )),
1647 )
1648 .ml_2(),
1649 )
1650 }
1651}
1652
1653fn initialize_new_editor(
1654 content: String,
1655 move_to_end: bool,
1656 window: &mut Window,
1657 cx: &mut App,
1658) -> Entity<Editor> {
1659 cx.new(|cx| {
1660 let mut editor = Editor::multi_line(window, cx);
1661 editor.hide_minimap_by_default(window, cx);
1662 editor.set_text(content, window, cx);
1663 editor.set_show_git_diff_gutter(false, cx);
1664 editor.set_show_runnables(false, cx);
1665 editor.set_show_breakpoints(false, cx);
1666 editor.set_read_only(true);
1667 editor.set_show_edit_predictions(Some(false), window, cx);
1668 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1669 if move_to_end {
1670 editor.move_to_end(&MoveToEnd, window, cx);
1671 }
1672 editor
1673 })
1674}
1675
1676const RPC_MESSAGES: &str = "RPC Messages";
1677const SERVER_LOGS: &str = "Server Logs";
1678const SERVER_TRACE: &str = "Server Trace";
1679const SERVER_INFO: &str = "Server Info";
1680
1681impl Default for LspLogToolbarItemView {
1682 fn default() -> Self {
1683 Self::new()
1684 }
1685}
1686
1687impl LspLogToolbarItemView {
1688 pub fn new() -> Self {
1689 Self {
1690 log_view: None,
1691 _log_view_subscription: None,
1692 }
1693 }
1694
1695 fn toggle_rpc_logging_for_server(
1696 &mut self,
1697 id: LanguageServerId,
1698 enabled: bool,
1699 window: &mut Window,
1700 cx: &mut Context<Self>,
1701 ) {
1702 if let Some(log_view) = &self.log_view {
1703 log_view.update(cx, |log_view, cx| {
1704 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1705 if !enabled && Some(id) == log_view.current_server_id {
1706 log_view.show_logs_for_server(id, window, cx);
1707 cx.notify();
1708 } else if enabled {
1709 log_view.show_rpc_trace_for_server(id, window, cx);
1710 cx.notify();
1711 }
1712 window.focus(&log_view.focus_handle);
1713 });
1714 }
1715 cx.notify();
1716 }
1717}
1718
1719pub enum Event {
1720 NewServerLogEntry {
1721 id: LanguageServerId,
1722 kind: LogKind,
1723 text: String,
1724 },
1725}
1726
1727impl EventEmitter<Event> for LogStore {}
1728impl EventEmitter<Event> for LspLogView {}
1729impl EventEmitter<EditorEvent> for LspLogView {}
1730impl EventEmitter<SearchEvent> for LspLogView {}