1use std::{collections::VecDeque, sync::Arc};
2
3use collections::HashMap;
4use futures::{StreamExt, channel::mpsc};
5use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, WeakEntity};
6use lsp::{
7 IoKind, LanguageServer, LanguageServerId, LanguageServerName, LanguageServerSelector,
8 MessageType, TraceValue,
9};
10use rpc::proto;
11use settings::WorktreeId;
12
13use crate::{LanguageServerLogType, LspStore, Project, ProjectItem as _};
14
15const SEND_LINE: &str = "\n// Send:";
16const RECEIVE_LINE: &str = "\n// Receive:";
17const MAX_STORED_LOG_ENTRIES: usize = 2000;
18
19pub fn init(on_headless_host: bool, cx: &mut App) -> Entity<LogStore> {
20 let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx));
21 cx.set_global(GlobalLogStore(log_store.clone()));
22 log_store
23}
24
25pub struct GlobalLogStore(pub Entity<LogStore>);
26
27impl Global for GlobalLogStore {}
28
29#[derive(Debug)]
30pub enum Event {
31 NewServerLogEntry {
32 id: LanguageServerId,
33 kind: LanguageServerLogType,
34 text: String,
35 },
36}
37
38impl EventEmitter<Event> for LogStore {}
39
40pub struct LogStore {
41 on_headless_host: bool,
42 projects: HashMap<WeakEntity<Project>, ProjectState>,
43 pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
44 io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
45}
46
47struct ProjectState {
48 _subscriptions: [Subscription; 2],
49 copilot_log_subscription: Option<lsp::Subscription>,
50}
51
52pub trait Message: AsRef<str> {
53 type Level: Copy + std::fmt::Debug;
54 fn should_include(&self, _: Self::Level) -> bool {
55 true
56 }
57}
58
59#[derive(Debug)]
60pub struct LogMessage {
61 message: String,
62 typ: MessageType,
63}
64
65impl AsRef<str> for LogMessage {
66 fn as_ref(&self) -> &str {
67 &self.message
68 }
69}
70
71impl Message for LogMessage {
72 type Level = MessageType;
73
74 fn should_include(&self, level: Self::Level) -> bool {
75 match (self.typ, level) {
76 (MessageType::ERROR, _) => true,
77 (_, MessageType::ERROR) => false,
78 (MessageType::WARNING, _) => true,
79 (_, MessageType::WARNING) => false,
80 (MessageType::INFO, _) => true,
81 (_, MessageType::INFO) => false,
82 _ => true,
83 }
84 }
85}
86
87#[derive(Debug)]
88pub struct TraceMessage {
89 message: String,
90 is_verbose: bool,
91}
92
93impl AsRef<str> for TraceMessage {
94 fn as_ref(&self) -> &str {
95 &self.message
96 }
97}
98
99impl Message for TraceMessage {
100 type Level = TraceValue;
101
102 fn should_include(&self, level: Self::Level) -> bool {
103 match level {
104 TraceValue::Off => false,
105 TraceValue::Messages => !self.is_verbose,
106 TraceValue::Verbose => true,
107 }
108 }
109}
110
111#[derive(Debug)]
112pub struct RpcMessage {
113 message: String,
114}
115
116impl AsRef<str> for RpcMessage {
117 fn as_ref(&self) -> &str {
118 &self.message
119 }
120}
121
122impl Message for RpcMessage {
123 type Level = ();
124}
125
126pub struct LanguageServerState {
127 pub name: Option<LanguageServerName>,
128 pub worktree_id: Option<WorktreeId>,
129 pub kind: LanguageServerKind,
130 log_messages: VecDeque<LogMessage>,
131 trace_messages: VecDeque<TraceMessage>,
132 pub rpc_state: Option<LanguageServerRpcState>,
133 pub trace_level: TraceValue,
134 pub log_level: MessageType,
135 io_logs_subscription: Option<lsp::Subscription>,
136 pub toggled_log_kind: Option<LogKind>,
137}
138
139impl std::fmt::Debug for LanguageServerState {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("LanguageServerState")
142 .field("name", &self.name)
143 .field("worktree_id", &self.worktree_id)
144 .field("kind", &self.kind)
145 .field("log_messages", &self.log_messages)
146 .field("trace_messages", &self.trace_messages)
147 .field("rpc_state", &self.rpc_state)
148 .field("trace_level", &self.trace_level)
149 .field("log_level", &self.log_level)
150 .field("toggled_log_kind", &self.toggled_log_kind)
151 .finish_non_exhaustive()
152 }
153}
154
155#[derive(PartialEq, Clone)]
156pub enum LanguageServerKind {
157 Local { project: WeakEntity<Project> },
158 Remote { project: WeakEntity<Project> },
159 LocalSsh { lsp_store: WeakEntity<LspStore> },
160 Global,
161}
162
163impl std::fmt::Debug for LanguageServerKind {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
167 LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
168 LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"),
169 LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
170 }
171 }
172}
173
174impl LanguageServerKind {
175 pub fn project(&self) -> Option<&WeakEntity<Project>> {
176 match self {
177 Self::Local { project } => Some(project),
178 Self::Remote { project } => Some(project),
179 Self::LocalSsh { .. } => None,
180 Self::Global { .. } => None,
181 }
182 }
183}
184
185#[derive(Debug)]
186pub struct LanguageServerRpcState {
187 pub rpc_messages: VecDeque<RpcMessage>,
188 last_message_kind: Option<MessageKind>,
189}
190
191#[derive(Debug, Copy, Clone, PartialEq, Eq)]
192enum MessageKind {
193 Send,
194 Receive,
195}
196
197#[derive(Clone, Copy, Debug, Default, PartialEq)]
198pub enum LogKind {
199 Rpc,
200 Trace,
201 #[default]
202 Logs,
203 ServerInfo,
204}
205
206impl LogKind {
207 pub fn from_server_log_type(log_type: &LanguageServerLogType) -> Self {
208 match log_type {
209 LanguageServerLogType::Log(_) => Self::Logs,
210 LanguageServerLogType::Trace { .. } => Self::Trace,
211 LanguageServerLogType::Rpc { .. } => Self::Rpc,
212 }
213 }
214}
215
216impl LogStore {
217 pub fn new(on_headless_host: bool, cx: &mut Context<Self>) -> Self {
218 let (io_tx, mut io_rx) = mpsc::unbounded();
219
220 let log_store = Self {
221 projects: HashMap::default(),
222 language_servers: HashMap::default(),
223
224 on_headless_host,
225 io_tx,
226 };
227 cx.spawn(async move |log_store, cx| {
228 while let Some((server_id, io_kind, message)) = io_rx.next().await {
229 if let Some(log_store) = log_store.upgrade() {
230 log_store.update(cx, |log_store, cx| {
231 log_store.on_io(server_id, io_kind, &message, cx);
232 });
233 }
234 }
235 anyhow::Ok(())
236 })
237 .detach_and_log_err(cx);
238
239 log_store
240 }
241
242 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
243 let weak_project = project.downgrade();
244 self.projects.insert(
245 project.downgrade(),
246 ProjectState {
247 _subscriptions: [
248 cx.observe_release(project, move |this, _, _| {
249 this.projects.remove(&weak_project);
250 this.language_servers
251 .retain(|_, state| state.kind.project() != Some(&weak_project));
252 }),
253 cx.subscribe(project, move |log_store, project, event, cx| {
254 let server_kind = if project.read(cx).is_local() {
255 LanguageServerKind::Local {
256 project: project.downgrade(),
257 }
258 } else {
259 LanguageServerKind::Remote {
260 project: project.downgrade(),
261 }
262 };
263 match event {
264 crate::Event::LanguageServerAdded(id, name, worktree_id) => {
265 log_store.add_language_server(
266 server_kind,
267 *id,
268 Some(name.clone()),
269 *worktree_id,
270 project
271 .read(cx)
272 .lsp_store()
273 .read(cx)
274 .language_server_for_id(*id),
275 cx,
276 );
277 }
278 crate::Event::LanguageServerBufferRegistered {
279 server_id,
280 buffer_id,
281 name,
282 ..
283 } => {
284 let worktree_id = project
285 .read(cx)
286 .buffer_for_id(*buffer_id, cx)
287 .and_then(|buffer| {
288 Some(buffer.read(cx).project_path(cx)?.worktree_id)
289 });
290 let name = name.clone().or_else(|| {
291 project
292 .read(cx)
293 .lsp_store()
294 .read(cx)
295 .language_server_statuses
296 .get(server_id)
297 .map(|status| status.name.clone())
298 });
299 log_store.add_language_server(
300 server_kind,
301 *server_id,
302 name,
303 worktree_id,
304 None,
305 cx,
306 );
307 }
308 crate::Event::LanguageServerRemoved(id) => {
309 log_store.remove_language_server(*id, cx);
310 }
311 crate::Event::LanguageServerLog(id, typ, message) => {
312 log_store.add_language_server(
313 server_kind,
314 *id,
315 None,
316 None,
317 None,
318 cx,
319 );
320 match typ {
321 crate::LanguageServerLogType::Log(typ) => {
322 log_store.add_language_server_log(*id, *typ, message, cx);
323 }
324 crate::LanguageServerLogType::Trace { verbose_info } => {
325 log_store.add_language_server_trace(
326 *id,
327 message,
328 verbose_info.clone(),
329 cx,
330 );
331 }
332 crate::LanguageServerLogType::Rpc { received } => {
333 let kind = if *received {
334 MessageKind::Receive
335 } else {
336 MessageKind::Send
337 };
338 log_store.add_language_server_rpc(*id, kind, message, cx);
339 }
340 }
341 }
342 crate::Event::ToggleLspLogs {
343 server_id,
344 enabled,
345 toggled_log_kind,
346 } => {
347 log_store.toggle_lsp_logs(*server_id, *enabled, *toggled_log_kind);
348 }
349 _ => {}
350 }
351 }),
352 ],
353 copilot_log_subscription: None,
354 },
355 );
356 }
357
358 pub fn get_language_server_state(
359 &mut self,
360 id: LanguageServerId,
361 ) -> Option<&mut LanguageServerState> {
362 self.language_servers.get_mut(&id)
363 }
364
365 pub fn add_language_server(
366 &mut self,
367 kind: LanguageServerKind,
368 server_id: LanguageServerId,
369 name: Option<LanguageServerName>,
370 worktree_id: Option<WorktreeId>,
371 server: Option<Arc<LanguageServer>>,
372 cx: &mut Context<Self>,
373 ) -> Option<&mut LanguageServerState> {
374 let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
375 cx.notify();
376 LanguageServerState {
377 name: None,
378 worktree_id: None,
379 kind,
380 rpc_state: None,
381 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
382 trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
383 trace_level: TraceValue::Off,
384 log_level: MessageType::LOG,
385 io_logs_subscription: None,
386 toggled_log_kind: None,
387 }
388 });
389
390 if let Some(name) = name {
391 server_state.name = Some(name);
392 }
393 if let Some(worktree_id) = worktree_id {
394 server_state.worktree_id = Some(worktree_id);
395 }
396
397 if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
398 let io_tx = self.io_tx.clone();
399 let server_id = server.server_id();
400 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
401 io_tx
402 .unbounded_send((server_id, io_kind, message.to_string()))
403 .ok();
404 }));
405 }
406
407 Some(server_state)
408 }
409
410 pub fn add_language_server_log(
411 &mut self,
412 id: LanguageServerId,
413 typ: MessageType,
414 message: &str,
415 cx: &mut Context<Self>,
416 ) -> Option<()> {
417 let store_logs = !self.on_headless_host;
418 let language_server_state = self.get_language_server_state(id)?;
419
420 let log_lines = &mut language_server_state.log_messages;
421 let message = message.trim_end().to_string();
422 if !store_logs {
423 // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway
424 self.emit_event(
425 Event::NewServerLogEntry {
426 id,
427 kind: LanguageServerLogType::Log(typ),
428 text: message,
429 },
430 cx,
431 );
432 } else if let Some(new_message) = Self::push_new_message(
433 log_lines,
434 LogMessage { message, typ },
435 language_server_state.log_level,
436 ) {
437 self.emit_event(
438 Event::NewServerLogEntry {
439 id,
440 kind: LanguageServerLogType::Log(typ),
441 text: new_message,
442 },
443 cx,
444 );
445 }
446 Some(())
447 }
448
449 fn add_language_server_trace(
450 &mut self,
451 id: LanguageServerId,
452 message: &str,
453 verbose_info: Option<String>,
454 cx: &mut Context<Self>,
455 ) -> Option<()> {
456 let store_logs = !self.on_headless_host;
457 let language_server_state = self.get_language_server_state(id)?;
458
459 let log_lines = &mut language_server_state.trace_messages;
460 if !store_logs {
461 // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway
462 self.emit_event(
463 Event::NewServerLogEntry {
464 id,
465 kind: LanguageServerLogType::Trace { verbose_info },
466 text: message.trim().to_string(),
467 },
468 cx,
469 );
470 } else if let Some(new_message) = Self::push_new_message(
471 log_lines,
472 TraceMessage {
473 message: message.trim().to_string(),
474 is_verbose: false,
475 },
476 TraceValue::Messages,
477 ) {
478 if let Some(verbose_message) = verbose_info.as_ref() {
479 Self::push_new_message(
480 log_lines,
481 TraceMessage {
482 message: verbose_message.clone(),
483 is_verbose: true,
484 },
485 TraceValue::Verbose,
486 );
487 }
488 self.emit_event(
489 Event::NewServerLogEntry {
490 id,
491 kind: LanguageServerLogType::Trace { verbose_info },
492 text: new_message,
493 },
494 cx,
495 );
496 }
497 Some(())
498 }
499
500 fn push_new_message<T: Message>(
501 log_lines: &mut VecDeque<T>,
502 message: T,
503 current_severity: <T as Message>::Level,
504 ) -> Option<String> {
505 while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
506 log_lines.pop_front();
507 }
508 let visible = message.should_include(current_severity);
509
510 let visible_message = visible.then(|| message.as_ref().to_string());
511 log_lines.push_back(message);
512 visible_message
513 }
514
515 fn add_language_server_rpc(
516 &mut self,
517 language_server_id: LanguageServerId,
518 kind: MessageKind,
519 message: &str,
520 cx: &mut Context<'_, Self>,
521 ) {
522 let store_logs = !self.on_headless_host;
523 let Some(state) = self
524 .get_language_server_state(language_server_id)
525 .and_then(|state| state.rpc_state.as_mut())
526 else {
527 return;
528 };
529
530 let received = kind == MessageKind::Receive;
531 let rpc_log_lines = &mut state.rpc_messages;
532 if state.last_message_kind != Some(kind) {
533 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
534 rpc_log_lines.pop_front();
535 }
536 let line_before_message = match kind {
537 MessageKind::Send => SEND_LINE,
538 MessageKind::Receive => RECEIVE_LINE,
539 };
540 if store_logs {
541 rpc_log_lines.push_back(RpcMessage {
542 message: line_before_message.to_string(),
543 });
544 }
545 // Do not send a synthetic message over the wire, it will be derived from the actual RPC message
546 cx.emit(Event::NewServerLogEntry {
547 id: language_server_id,
548 kind: LanguageServerLogType::Rpc { received },
549 text: line_before_message.to_string(),
550 });
551 }
552
553 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
554 rpc_log_lines.pop_front();
555 }
556
557 if store_logs {
558 rpc_log_lines.push_back(RpcMessage {
559 message: message.trim().to_owned(),
560 });
561 }
562
563 self.emit_event(
564 Event::NewServerLogEntry {
565 id: language_server_id,
566 kind: LanguageServerLogType::Rpc { received },
567 text: message.to_owned(),
568 },
569 cx,
570 );
571 }
572
573 pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
574 self.language_servers.remove(&id);
575 cx.notify();
576 }
577
578 pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
579 Some(&self.language_servers.get(&server_id)?.log_messages)
580 }
581
582 pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
583 Some(&self.language_servers.get(&server_id)?.trace_messages)
584 }
585
586 pub fn server_ids_for_project<'a>(
587 &'a self,
588 lookup_project: &'a WeakEntity<Project>,
589 ) -> impl Iterator<Item = LanguageServerId> + 'a {
590 self.language_servers
591 .iter()
592 .filter_map(move |(id, state)| match &state.kind {
593 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
594 if project == lookup_project {
595 Some(*id)
596 } else {
597 None
598 }
599 }
600 LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id),
601 })
602 }
603
604 pub fn enable_rpc_trace_for_language_server(
605 &mut self,
606 server_id: LanguageServerId,
607 ) -> Option<&mut LanguageServerRpcState> {
608 let rpc_state = self
609 .language_servers
610 .get_mut(&server_id)?
611 .rpc_state
612 .get_or_insert_with(|| LanguageServerRpcState {
613 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
614 last_message_kind: None,
615 });
616 Some(rpc_state)
617 }
618
619 pub fn disable_rpc_trace_for_language_server(
620 &mut self,
621 server_id: LanguageServerId,
622 ) -> Option<()> {
623 self.language_servers.get_mut(&server_id)?.rpc_state.take();
624 Some(())
625 }
626
627 pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
628 match server {
629 LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
630 LanguageServerSelector::Name(name) => self
631 .language_servers
632 .iter()
633 .any(|(_, state)| state.name.as_ref() == Some(name)),
634 }
635 }
636
637 fn on_io(
638 &mut self,
639 language_server_id: LanguageServerId,
640 io_kind: IoKind,
641 message: &str,
642 cx: &mut Context<Self>,
643 ) -> Option<()> {
644 let is_received = match io_kind {
645 IoKind::StdOut => true,
646 IoKind::StdIn => false,
647 IoKind::StdErr => {
648 self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
649 return Some(());
650 }
651 };
652
653 let kind = if is_received {
654 MessageKind::Receive
655 } else {
656 MessageKind::Send
657 };
658
659 self.add_language_server_rpc(language_server_id, kind, message, cx);
660 cx.notify();
661 Some(())
662 }
663
664 fn emit_event(&mut self, e: Event, cx: &mut Context<Self>) {
665 match &e {
666 Event::NewServerLogEntry { id, kind, text } => {
667 if let Some(state) = self.get_language_server_state(*id) {
668 let downstream_client = match &state.kind {
669 LanguageServerKind::Remote { project }
670 | LanguageServerKind::Local { project } => project
671 .upgrade()
672 .map(|project| project.read(cx).lsp_store()),
673 LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(),
674 LanguageServerKind::Global => None,
675 }
676 .and_then(|lsp_store| lsp_store.read(cx).downstream_client());
677 if let Some((client, project_id)) = downstream_client {
678 if Some(LogKind::from_server_log_type(kind)) == state.toggled_log_kind {
679 client
680 .send(proto::LanguageServerLog {
681 project_id,
682 language_server_id: id.to_proto(),
683 message: text.clone(),
684 log_type: Some(kind.to_proto()),
685 })
686 .ok();
687 }
688 }
689 }
690 }
691 }
692
693 cx.emit(e);
694 }
695
696 pub fn toggle_lsp_logs(
697 &mut self,
698 server_id: LanguageServerId,
699 enabled: bool,
700 toggled_log_kind: LogKind,
701 ) {
702 if let Some(server_state) = self.get_language_server_state(server_id) {
703 if enabled {
704 server_state.toggled_log_kind = Some(toggled_log_kind);
705 } else {
706 server_state.toggled_log_kind = None;
707 }
708 }
709 if LogKind::Rpc == toggled_log_kind {
710 if enabled {
711 self.enable_rpc_trace_for_language_server(server_id);
712 } else {
713 self.disable_rpc_trace_for_language_server(server_id);
714 }
715 }
716 }
717 pub fn copilot_state_for_project(
718 &mut self,
719 project: &WeakEntity<Project>,
720 ) -> Option<&mut Option<lsp::Subscription>> {
721 self.projects
722 .get_mut(project)
723 .map(|project| &mut project.copilot_log_subscription)
724 }
725}