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