1use collections::{HashMap, VecDeque};
2use copilot::Copilot;
3use editor::{actions::MoveToEnd, Editor, EditorEvent};
4use futures::{channel::mpsc, StreamExt};
5use gpui::{
6 actions, div, AnchorCorner, AppContext, Context, EventEmitter, FocusHandle, FocusableView,
7 IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, View,
8 ViewContext, VisualContext, WeakModel, WindowContext,
9};
10use language::{LanguageServerId, LanguageServerName};
11use lsp::{
12 notification::SetTrace, IoKind, LanguageServer, MessageType, SetTraceParams, TraceValue,
13};
14use project::{search::SearchQuery, Project, WorktreeId};
15use std::{borrow::Cow, sync::Arc};
16use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
17use workspace::{
18 item::{Item, ItemHandle},
19 searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
20 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
21};
22
23const SEND_LINE: &str = "// Send:";
24const RECEIVE_LINE: &str = "// Receive:";
25const MAX_STORED_LOG_ENTRIES: usize = 2000;
26
27pub struct LogStore {
28 projects: HashMap<WeakModel<Project>, ProjectState>,
29 language_servers: HashMap<LanguageServerId, LanguageServerState>,
30 copilot_log_subscription: Option<lsp::Subscription>,
31 _copilot_subscription: Option<gpui::Subscription>,
32 io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
33}
34
35struct ProjectState {
36 _subscriptions: [gpui::Subscription; 2],
37}
38
39trait Message: AsRef<str> {
40 type Level: Copy + std::fmt::Debug;
41 fn should_include(&self, _: Self::Level) -> bool {
42 true
43 }
44}
45
46struct LogMessage {
47 message: String,
48 typ: MessageType,
49}
50
51impl AsRef<str> for LogMessage {
52 fn as_ref(&self) -> &str {
53 &self.message
54 }
55}
56
57impl Message for LogMessage {
58 type Level = MessageType;
59
60 fn should_include(&self, level: Self::Level) -> bool {
61 match (self.typ, level) {
62 (MessageType::ERROR, _) => true,
63 (_, MessageType::ERROR) => false,
64 (MessageType::WARNING, _) => true,
65 (_, MessageType::WARNING) => false,
66 (MessageType::INFO, _) => true,
67 (_, MessageType::INFO) => false,
68 _ => true,
69 }
70 }
71}
72
73struct TraceMessage {
74 message: String,
75}
76
77impl AsRef<str> for TraceMessage {
78 fn as_ref(&self) -> &str {
79 &self.message
80 }
81}
82
83impl Message for TraceMessage {
84 type Level = ();
85}
86
87struct RpcMessage {
88 message: String,
89}
90
91impl AsRef<str> for RpcMessage {
92 fn as_ref(&self) -> &str {
93 &self.message
94 }
95}
96
97impl Message for RpcMessage {
98 type Level = ();
99}
100
101struct LanguageServerState {
102 name: Option<LanguageServerName>,
103 worktree_id: Option<WorktreeId>,
104 kind: LanguageServerKind,
105 log_messages: VecDeque<LogMessage>,
106 trace_messages: VecDeque<TraceMessage>,
107 rpc_state: Option<LanguageServerRpcState>,
108 trace_level: TraceValue,
109 log_level: MessageType,
110 io_logs_subscription: Option<lsp::Subscription>,
111}
112
113#[derive(PartialEq, Clone)]
114pub enum LanguageServerKind {
115 Local { project: WeakModel<Project> },
116 Remote { project: WeakModel<Project> },
117 Global,
118}
119
120impl LanguageServerKind {
121 fn is_remote(&self) -> bool {
122 matches!(self, LanguageServerKind::Remote { .. })
123 }
124}
125
126impl std::fmt::Debug for LanguageServerKind {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
130 LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
131 LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
132 }
133 }
134}
135
136impl LanguageServerKind {
137 fn project(&self) -> Option<&WeakModel<Project>> {
138 match self {
139 Self::Local { project } => Some(project),
140 Self::Remote { project } => Some(project),
141 Self::Global { .. } => None,
142 }
143 }
144}
145
146struct LanguageServerRpcState {
147 rpc_messages: VecDeque<RpcMessage>,
148 last_message_kind: Option<MessageKind>,
149}
150
151pub struct LspLogView {
152 pub(crate) editor: View<Editor>,
153 editor_subscriptions: Vec<Subscription>,
154 log_store: Model<LogStore>,
155 current_server_id: Option<LanguageServerId>,
156 active_entry_kind: LogKind,
157 project: Model<Project>,
158 focus_handle: FocusHandle,
159 _log_store_subscriptions: Vec<Subscription>,
160}
161
162pub struct LspLogToolbarItemView {
163 log_view: Option<View<LspLogView>>,
164 _log_view_subscription: Option<Subscription>,
165}
166
167#[derive(Copy, Clone, PartialEq, Eq)]
168enum MessageKind {
169 Send,
170 Receive,
171}
172
173#[derive(Clone, Copy, Debug, Default, PartialEq)]
174pub enum LogKind {
175 Rpc,
176 Trace,
177 #[default]
178 Logs,
179}
180
181impl LogKind {
182 fn label(&self) -> &'static str {
183 match self {
184 LogKind::Rpc => RPC_MESSAGES,
185 LogKind::Trace => SERVER_TRACE,
186 LogKind::Logs => SERVER_LOGS,
187 }
188 }
189}
190
191#[derive(Clone, Debug, PartialEq)]
192pub(crate) struct LogMenuItem {
193 pub server_id: LanguageServerId,
194 pub server_name: LanguageServerName,
195 pub worktree_root_name: String,
196 pub rpc_trace_enabled: bool,
197 pub selected_entry: LogKind,
198 pub trace_level: lsp::TraceValue,
199 pub server_kind: LanguageServerKind,
200}
201
202actions!(debug, [OpenLanguageServerLogs]);
203
204pub fn init(cx: &mut AppContext) {
205 let log_store = cx.new_model(LogStore::new);
206
207 cx.observe_new_views(move |workspace: &mut Workspace, cx| {
208 let project = workspace.project();
209 if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
210 log_store.update(cx, |store, cx| {
211 store.add_project(project, cx);
212 });
213 }
214
215 let log_store = log_store.clone();
216 workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
217 let project = workspace.project().read(cx);
218 if project.is_local() || project.is_via_ssh() {
219 workspace.split_item(
220 SplitDirection::Right,
221 Box::new(cx.new_view(|cx| {
222 LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
223 })),
224 cx,
225 );
226 }
227 });
228 })
229 .detach();
230}
231
232impl LogStore {
233 pub fn new(cx: &mut ModelContext<Self>) -> Self {
234 let (io_tx, mut io_rx) = mpsc::unbounded();
235
236 let copilot_subscription = Copilot::global(cx).map(|copilot| {
237 let copilot = &copilot;
238 cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| {
239 if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event {
240 if let Some(server) = copilot.read(cx).language_server() {
241 let server_id = server.server_id();
242 let weak_this = cx.weak_model();
243 this.copilot_log_subscription =
244 Some(server.on_notification::<copilot::request::LogMessage, _>(
245 move |params, mut cx| {
246 weak_this
247 .update(&mut cx, |this, cx| {
248 this.add_language_server_log(
249 server_id,
250 MessageType::LOG,
251 ¶ms.message,
252 cx,
253 );
254 })
255 .ok();
256 },
257 ));
258 let name = LanguageServerName::new_static("copilot");
259 this.add_language_server(
260 LanguageServerKind::Global,
261 server.server_id(),
262 Some(name),
263 None,
264 Some(server.clone()),
265 cx,
266 );
267 }
268 }
269 })
270 });
271
272 let this = Self {
273 copilot_log_subscription: None,
274 _copilot_subscription: copilot_subscription,
275 projects: HashMap::default(),
276 language_servers: HashMap::default(),
277 io_tx,
278 };
279
280 cx.spawn(|this, mut cx| async move {
281 while let Some((server_id, io_kind, message)) = io_rx.next().await {
282 if let Some(this) = this.upgrade() {
283 this.update(&mut cx, |this, cx| {
284 this.on_io(server_id, io_kind, &message, cx);
285 })?;
286 }
287 }
288 anyhow::Ok(())
289 })
290 .detach_and_log_err(cx);
291 this
292 }
293
294 pub fn add_project(&mut self, project: &Model<Project>, cx: &mut ModelContext<Self>) {
295 let weak_project = project.downgrade();
296 self.projects.insert(
297 project.downgrade(),
298 ProjectState {
299 _subscriptions: [
300 cx.observe_release(project, move |this, _, _| {
301 this.projects.remove(&weak_project);
302 this.language_servers
303 .retain(|_, state| state.kind.project() != Some(&weak_project));
304 }),
305 cx.subscribe(project, |this, project, event, cx| {
306 let server_kind = if project.read(cx).is_via_ssh() {
307 LanguageServerKind::Remote {
308 project: project.downgrade(),
309 }
310 } else {
311 LanguageServerKind::Local {
312 project: project.downgrade(),
313 }
314 };
315
316 match event {
317 project::Event::LanguageServerAdded(id, name, worktree_id) => {
318 this.add_language_server(
319 server_kind,
320 *id,
321 Some(name.clone()),
322 *worktree_id,
323 project.read(cx).language_server_for_id(*id, cx),
324 cx,
325 );
326 }
327 project::Event::LanguageServerRemoved(id) => {
328 this.remove_language_server(*id, cx);
329 }
330 project::Event::LanguageServerLog(id, typ, message) => {
331 this.add_language_server(server_kind, *id, None, None, None, cx);
332 match typ {
333 project::LanguageServerLogType::Log(typ) => {
334 this.add_language_server_log(*id, *typ, message, cx);
335 }
336 project::LanguageServerLogType::Trace(_) => {
337 this.add_language_server_trace(*id, message, cx);
338 }
339 }
340 }
341 _ => {}
342 }
343 }),
344 ],
345 },
346 );
347 }
348
349 fn get_language_server_state(
350 &mut self,
351 id: LanguageServerId,
352 ) -> Option<&mut LanguageServerState> {
353 self.language_servers.get_mut(&id)
354 }
355
356 fn add_language_server(
357 &mut self,
358 kind: LanguageServerKind,
359 server_id: LanguageServerId,
360 name: Option<LanguageServerName>,
361 worktree_id: Option<WorktreeId>,
362 server: Option<Arc<LanguageServer>>,
363 cx: &mut ModelContext<Self>,
364 ) -> Option<&mut LanguageServerState> {
365 let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
366 cx.notify();
367 LanguageServerState {
368 name: None,
369 worktree_id: None,
370 kind,
371 rpc_state: None,
372 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
373 trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
374 trace_level: TraceValue::Off,
375 log_level: MessageType::LOG,
376 io_logs_subscription: None,
377 }
378 });
379
380 if let Some(name) = name {
381 server_state.name = Some(name);
382 }
383 if let Some(worktree_id) = worktree_id {
384 server_state.worktree_id = Some(worktree_id);
385 }
386
387 if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
388 let io_tx = self.io_tx.clone();
389 let server_id = server.server_id();
390 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
391 io_tx
392 .unbounded_send((server_id, io_kind, message.to_string()))
393 .ok();
394 }));
395 }
396 Some(server_state)
397 }
398
399 fn add_language_server_log(
400 &mut self,
401 id: LanguageServerId,
402 typ: MessageType,
403 message: &str,
404 cx: &mut ModelContext<Self>,
405 ) -> Option<()> {
406 let language_server_state = self.get_language_server_state(id)?;
407
408 let log_lines = &mut language_server_state.log_messages;
409 Self::add_language_server_message(
410 log_lines,
411 id,
412 LogMessage {
413 message: message.trim_end().to_string(),
414 typ,
415 },
416 language_server_state.log_level,
417 LogKind::Logs,
418 cx,
419 );
420 Some(())
421 }
422
423 fn add_language_server_trace(
424 &mut self,
425 id: LanguageServerId,
426 message: &str,
427 cx: &mut ModelContext<Self>,
428 ) -> Option<()> {
429 let language_server_state = self.get_language_server_state(id)?;
430
431 let log_lines = &mut language_server_state.trace_messages;
432 Self::add_language_server_message(
433 log_lines,
434 id,
435 TraceMessage {
436 message: message.trim_end().to_string(),
437 },
438 (),
439 LogKind::Trace,
440 cx,
441 );
442 Some(())
443 }
444
445 fn add_language_server_message<T: Message>(
446 log_lines: &mut VecDeque<T>,
447 id: LanguageServerId,
448 message: T,
449 current_severity: <T as Message>::Level,
450 kind: LogKind,
451 cx: &mut ModelContext<Self>,
452 ) {
453 while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
454 log_lines.pop_front();
455 }
456 let entry: &str = message.as_ref();
457 let entry = entry.to_string();
458 let visible = message.should_include(current_severity);
459 log_lines.push_back(message);
460
461 if visible {
462 cx.emit(Event::NewServerLogEntry { id, entry, kind });
463 cx.notify();
464 }
465 }
466
467 fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext<Self>) {
468 self.language_servers.remove(&id);
469 cx.notify();
470 }
471
472 fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
473 Some(&self.language_servers.get(&server_id)?.log_messages)
474 }
475
476 fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
477 Some(&self.language_servers.get(&server_id)?.trace_messages)
478 }
479
480 fn server_ids_for_project<'a>(
481 &'a self,
482 lookup_project: &'a WeakModel<Project>,
483 ) -> impl Iterator<Item = LanguageServerId> + 'a {
484 self.language_servers
485 .iter()
486 .filter_map(move |(id, state)| match &state.kind {
487 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
488 if project == lookup_project {
489 Some(*id)
490 } else {
491 None
492 }
493 }
494 LanguageServerKind::Global => Some(*id),
495 })
496 }
497
498 fn enable_rpc_trace_for_language_server(
499 &mut self,
500 server_id: LanguageServerId,
501 ) -> Option<&mut LanguageServerRpcState> {
502 let rpc_state = self
503 .language_servers
504 .get_mut(&server_id)?
505 .rpc_state
506 .get_or_insert_with(|| LanguageServerRpcState {
507 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
508 last_message_kind: None,
509 });
510 Some(rpc_state)
511 }
512
513 pub fn disable_rpc_trace_for_language_server(
514 &mut self,
515 server_id: LanguageServerId,
516 ) -> Option<()> {
517 self.language_servers.get_mut(&server_id)?.rpc_state.take();
518 Some(())
519 }
520
521 fn on_io(
522 &mut self,
523 language_server_id: LanguageServerId,
524 io_kind: IoKind,
525 message: &str,
526 cx: &mut ModelContext<Self>,
527 ) -> Option<()> {
528 let is_received = match io_kind {
529 IoKind::StdOut => true,
530 IoKind::StdIn => false,
531 IoKind::StdErr => {
532 let message = format!("stderr: {}", message.trim());
533 self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
534 return Some(());
535 }
536 };
537
538 let state = self
539 .get_language_server_state(language_server_id)?
540 .rpc_state
541 .as_mut()?;
542 let kind = if is_received {
543 MessageKind::Receive
544 } else {
545 MessageKind::Send
546 };
547
548 let rpc_log_lines = &mut state.rpc_messages;
549 if state.last_message_kind != Some(kind) {
550 let line_before_message = match kind {
551 MessageKind::Send => SEND_LINE,
552 MessageKind::Receive => RECEIVE_LINE,
553 };
554 rpc_log_lines.push_back(RpcMessage {
555 message: line_before_message.to_string(),
556 });
557 cx.emit(Event::NewServerLogEntry {
558 id: language_server_id,
559 entry: line_before_message.to_string(),
560 kind: LogKind::Rpc,
561 });
562 }
563
564 while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
565 rpc_log_lines.pop_front();
566 }
567 let message = message.trim();
568 rpc_log_lines.push_back(RpcMessage {
569 message: message.to_string(),
570 });
571 cx.emit(Event::NewServerLogEntry {
572 id: language_server_id,
573 entry: message.to_string(),
574 kind: LogKind::Rpc,
575 });
576 cx.notify();
577 Some(())
578 }
579}
580
581impl LspLogView {
582 pub fn new(
583 project: Model<Project>,
584 log_store: Model<LogStore>,
585 cx: &mut ViewContext<Self>,
586 ) -> Self {
587 let server_id = log_store
588 .read(cx)
589 .language_servers
590 .iter()
591 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
592 .map(|(id, _)| *id);
593
594 let weak_project = project.downgrade();
595 let model_changes_subscription = cx.observe(&log_store, move |this, store, cx| {
596 let first_server_id_for_project =
597 store.read(cx).server_ids_for_project(&weak_project).next();
598 if let Some(current_lsp) = this.current_server_id {
599 if !store.read(cx).language_servers.contains_key(¤t_lsp) {
600 if let Some(server_id) = first_server_id_for_project {
601 match this.active_entry_kind {
602 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
603 LogKind::Trace => this.show_trace_for_server(server_id, cx),
604 LogKind::Logs => this.show_logs_for_server(server_id, cx),
605 }
606 } else {
607 this.current_server_id = None;
608 this.editor.update(cx, |editor, cx| {
609 editor.set_read_only(false);
610 editor.clear(cx);
611 editor.set_read_only(true);
612 });
613 cx.notify();
614 }
615 }
616 } else if let Some(server_id) = first_server_id_for_project {
617 match this.active_entry_kind {
618 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
619 LogKind::Trace => this.show_trace_for_server(server_id, cx),
620 LogKind::Logs => this.show_logs_for_server(server_id, cx),
621 }
622 }
623
624 cx.notify();
625 });
626 let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
627 Event::NewServerLogEntry { id, entry, kind } => {
628 if log_view.current_server_id == Some(*id) && *kind == log_view.active_entry_kind {
629 log_view.editor.update(cx, |editor, cx| {
630 editor.set_read_only(false);
631 let last_point = editor.buffer().read(cx).len(cx);
632 editor.edit(
633 vec![
634 (last_point..last_point, entry.trim()),
635 (last_point..last_point, "\n"),
636 ],
637 cx,
638 );
639 editor.set_read_only(true);
640 });
641 }
642 }
643 });
644 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx);
645
646 let focus_handle = cx.focus_handle();
647 let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
648 cx.focus_view(&log_view.editor);
649 });
650
651 let mut this = Self {
652 focus_handle,
653 editor,
654 editor_subscriptions,
655 project,
656 log_store,
657 current_server_id: None,
658 active_entry_kind: LogKind::Logs,
659 _log_store_subscriptions: vec![
660 model_changes_subscription,
661 events_subscriptions,
662 focus_subscription,
663 ],
664 };
665 if let Some(server_id) = server_id {
666 this.show_logs_for_server(server_id, cx);
667 }
668 this
669 }
670
671 fn editor_for_logs(
672 log_contents: String,
673 cx: &mut ViewContext<Self>,
674 ) -> (View<Editor>, Vec<Subscription>) {
675 let editor = cx.new_view(|cx| {
676 let mut editor = Editor::multi_line(cx);
677 editor.set_text(log_contents, cx);
678 editor.move_to_end(&MoveToEnd, cx);
679 editor.set_read_only(true);
680 editor.set_show_inline_completions(Some(false), cx);
681 editor
682 });
683 let editor_subscription = cx.subscribe(
684 &editor,
685 |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
686 cx.emit(event.clone())
687 },
688 );
689 let search_subscription = cx.subscribe(
690 &editor,
691 |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, LspLogView>| {
692 cx.emit(event.clone())
693 },
694 );
695 (editor, vec![editor_subscription, search_subscription])
696 }
697
698 pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
699 let log_store = self.log_store.read(cx);
700
701 let unknown_server = LanguageServerName::new_static("unknown server");
702
703 let mut rows = log_store
704 .language_servers
705 .iter()
706 .filter_map(|(server_id, state)| match &state.kind {
707 LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
708 let worktree_root_name = state
709 .worktree_id
710 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
711 .map(|worktree| worktree.read(cx).root_name().to_string())
712 .unwrap_or_else(|| "Unknown worktree".to_string());
713
714 let state = log_store.language_servers.get(&server_id)?;
715 Some(LogMenuItem {
716 server_id: *server_id,
717 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
718 server_kind: state.kind.clone(),
719 worktree_root_name,
720 rpc_trace_enabled: state.rpc_state.is_some(),
721 selected_entry: self.active_entry_kind,
722 trace_level: lsp::TraceValue::Off,
723 })
724 }
725
726 LanguageServerKind::Global => Some(LogMenuItem {
727 server_id: *server_id,
728 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
729 server_kind: state.kind.clone(),
730 worktree_root_name: "supplementary".to_string(),
731 rpc_trace_enabled: state.rpc_state.is_some(),
732 selected_entry: self.active_entry_kind,
733 trace_level: lsp::TraceValue::Off,
734 }),
735 })
736 .chain(
737 self.project
738 .read(cx)
739 .supplementary_language_servers(cx)
740 .filter_map(|(server_id, name)| {
741 let state = log_store.language_servers.get(&server_id)?;
742 Some(LogMenuItem {
743 server_id,
744 server_name: name.clone(),
745 server_kind: state.kind.clone(),
746 worktree_root_name: "supplementary".to_string(),
747 rpc_trace_enabled: state.rpc_state.is_some(),
748 selected_entry: self.active_entry_kind,
749 trace_level: lsp::TraceValue::Off,
750 })
751 }),
752 )
753 .collect::<Vec<_>>();
754 rows.sort_by_key(|row| row.server_id);
755 rows.dedup_by_key(|row| row.server_id);
756 Some(rows)
757 }
758
759 fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
760 let typ = self
761 .log_store
762 .read_with(cx, |v, _| {
763 v.language_servers.get(&server_id).map(|v| v.log_level)
764 })
765 .unwrap_or(MessageType::LOG);
766 let log_contents = self
767 .log_store
768 .read(cx)
769 .server_logs(server_id)
770 .map(|v| log_contents(v, typ));
771 if let Some(log_contents) = log_contents {
772 self.current_server_id = Some(server_id);
773 self.active_entry_kind = LogKind::Logs;
774 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
775 self.editor = editor;
776 self.editor_subscriptions = editor_subscriptions;
777 cx.notify();
778 }
779 cx.focus(&self.focus_handle);
780 }
781
782 fn update_log_level(
783 &self,
784 server_id: LanguageServerId,
785 level: MessageType,
786 cx: &mut ViewContext<Self>,
787 ) {
788 let log_contents = self.log_store.update(cx, |this, _| {
789 if let Some(state) = this.get_language_server_state(server_id) {
790 state.log_level = level;
791 }
792
793 this.server_logs(server_id).map(|v| log_contents(v, level))
794 });
795
796 if let Some(log_contents) = log_contents {
797 self.editor.update(cx, move |editor, cx| {
798 editor.set_text(log_contents, cx);
799 editor.move_to_end(&MoveToEnd, cx);
800 });
801 cx.notify();
802 }
803
804 cx.focus(&self.focus_handle);
805 }
806
807 fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
808 let log_contents = self
809 .log_store
810 .read(cx)
811 .server_trace(server_id)
812 .map(|v| log_contents(v, ()));
813 if let Some(log_contents) = log_contents {
814 self.current_server_id = Some(server_id);
815 self.active_entry_kind = LogKind::Trace;
816 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
817 self.editor = editor;
818 self.editor_subscriptions = editor_subscriptions;
819 cx.notify();
820 }
821 cx.focus(&self.focus_handle);
822 }
823
824 fn show_rpc_trace_for_server(
825 &mut self,
826 server_id: LanguageServerId,
827 cx: &mut ViewContext<Self>,
828 ) {
829 let rpc_log = self.log_store.update(cx, |log_store, _| {
830 log_store
831 .enable_rpc_trace_for_language_server(server_id)
832 .map(|state| log_contents(&state.rpc_messages, ()))
833 });
834 if let Some(rpc_log) = rpc_log {
835 self.current_server_id = Some(server_id);
836 self.active_entry_kind = LogKind::Rpc;
837 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx);
838 let language = self.project.read(cx).languages().language_for_name("JSON");
839 editor
840 .read(cx)
841 .buffer()
842 .read(cx)
843 .as_singleton()
844 .expect("log buffer should be a singleton")
845 .update(cx, |_, cx| {
846 cx.spawn({
847 let buffer = cx.handle();
848 |_, mut cx| async move {
849 let language = language.await.ok();
850 buffer.update(&mut cx, |buffer, cx| {
851 buffer.set_language(language, cx);
852 })
853 }
854 })
855 .detach_and_log_err(cx);
856 });
857
858 self.editor = editor;
859 self.editor_subscriptions = editor_subscriptions;
860 cx.notify();
861 }
862
863 cx.focus(&self.focus_handle);
864 }
865
866 fn toggle_rpc_trace_for_server(
867 &mut self,
868 server_id: LanguageServerId,
869 enabled: bool,
870 cx: &mut ViewContext<Self>,
871 ) {
872 self.log_store.update(cx, |log_store, _| {
873 if enabled {
874 log_store.enable_rpc_trace_for_language_server(server_id);
875 } else {
876 log_store.disable_rpc_trace_for_language_server(server_id);
877 }
878 });
879 if !enabled && Some(server_id) == self.current_server_id {
880 self.show_logs_for_server(server_id, cx);
881 cx.notify();
882 }
883 }
884 fn update_trace_level(
885 &self,
886 server_id: LanguageServerId,
887 level: TraceValue,
888 cx: &mut ViewContext<Self>,
889 ) {
890 if let Some(server) = self.project.read(cx).language_server_for_id(server_id, cx) {
891 self.log_store.update(cx, |this, _| {
892 if let Some(state) = this.get_language_server_state(server_id) {
893 state.trace_level = level;
894 }
895 });
896
897 server
898 .notify::<SetTrace>(SetTraceParams { value: level })
899 .ok();
900 }
901 }
902}
903
904fn log_filter<T: Message>(line: &T, cmp: <T as Message>::Level) -> Option<&str> {
905 if line.should_include(cmp) {
906 Some(line.as_ref())
907 } else {
908 None
909 }
910}
911
912fn log_contents<T: Message>(lines: &VecDeque<T>, cmp: <T as Message>::Level) -> String {
913 let (a, b) = lines.as_slices();
914 let a = a.iter().filter_map(move |v| log_filter(v, cmp));
915 let b = b.iter().filter_map(move |v| log_filter(v, cmp));
916 a.chain(b).fold(String::new(), |mut acc, el| {
917 acc.push_str(el);
918 acc.push('\n');
919 acc
920 })
921}
922
923impl Render for LspLogView {
924 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
925 self.editor
926 .update(cx, |editor, cx| editor.render(cx).into_any_element())
927 }
928}
929
930impl FocusableView for LspLogView {
931 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
932 self.focus_handle.clone()
933 }
934}
935
936impl Item for LspLogView {
937 type Event = EditorEvent;
938
939 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
940 Editor::to_item_events(event, f)
941 }
942
943 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
944 Some("LSP Logs".into())
945 }
946
947 fn telemetry_event_text(&self) -> Option<&'static str> {
948 None
949 }
950
951 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
952 Some(Box::new(handle.clone()))
953 }
954
955 fn clone_on_split(
956 &self,
957 _workspace_id: Option<WorkspaceId>,
958 cx: &mut ViewContext<Self>,
959 ) -> Option<View<Self>>
960 where
961 Self: Sized,
962 {
963 Some(cx.new_view(|cx| {
964 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), cx);
965 if let Some(server_id) = self.current_server_id {
966 match self.active_entry_kind {
967 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, cx),
968 LogKind::Trace => new_view.show_trace_for_server(server_id, cx),
969 LogKind::Logs => new_view.show_logs_for_server(server_id, cx),
970 }
971 }
972 new_view
973 }))
974 }
975}
976
977impl SearchableItem for LspLogView {
978 type Match = <Editor as SearchableItem>::Match;
979
980 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
981 self.editor.update(cx, |e, cx| e.clear_matches(cx))
982 }
983
984 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
985 self.editor
986 .update(cx, |e, cx| e.update_matches(matches, cx))
987 }
988
989 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
990 self.editor.update(cx, |e, cx| e.query_suggestion(cx))
991 }
992
993 fn activate_match(
994 &mut self,
995 index: usize,
996 matches: &[Self::Match],
997 cx: &mut ViewContext<Self>,
998 ) {
999 self.editor
1000 .update(cx, |e, cx| e.activate_match(index, matches, cx))
1001 }
1002
1003 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1004 self.editor
1005 .update(cx, |e, cx| e.select_matches(matches, cx))
1006 }
1007
1008 fn find_matches(
1009 &mut self,
1010 query: Arc<project::search::SearchQuery>,
1011 cx: &mut ViewContext<Self>,
1012 ) -> gpui::Task<Vec<Self::Match>> {
1013 self.editor.update(cx, |e, cx| e.find_matches(query, cx))
1014 }
1015
1016 fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
1017 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1018 }
1019 fn supported_options() -> workspace::searchable::SearchOptions {
1020 workspace::searchable::SearchOptions {
1021 case: true,
1022 word: true,
1023 regex: true,
1024 // LSP log is read-only.
1025 replacement: false,
1026 selection: false,
1027 }
1028 }
1029 fn active_match_index(
1030 &mut self,
1031 matches: &[Self::Match],
1032 cx: &mut ViewContext<Self>,
1033 ) -> Option<usize> {
1034 self.editor
1035 .update(cx, |e, cx| e.active_match_index(matches, cx))
1036 }
1037}
1038
1039impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1040
1041impl ToolbarItemView for LspLogToolbarItemView {
1042 fn set_active_pane_item(
1043 &mut self,
1044 active_pane_item: Option<&dyn ItemHandle>,
1045 cx: &mut ViewContext<Self>,
1046 ) -> workspace::ToolbarItemLocation {
1047 if let Some(item) = active_pane_item {
1048 if let Some(log_view) = item.downcast::<LspLogView>() {
1049 self.log_view = Some(log_view.clone());
1050 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1051 cx.notify();
1052 }));
1053 return ToolbarItemLocation::PrimaryLeft;
1054 }
1055 }
1056 self.log_view = None;
1057 self._log_view_subscription = None;
1058 ToolbarItemLocation::Hidden
1059 }
1060}
1061
1062impl Render for LspLogToolbarItemView {
1063 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1064 let Some(log_view) = self.log_view.clone() else {
1065 return div();
1066 };
1067 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1068 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1069 let current_server_id = log_view.current_server_id;
1070 (menu_rows, current_server_id)
1071 });
1072
1073 let current_server = current_server_id.and_then(|current_server_id| {
1074 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1075 Some(menu_rows[ix].clone())
1076 } else {
1077 None
1078 }
1079 });
1080
1081 let log_toolbar_view = cx.view().clone();
1082 let lsp_menu = PopoverMenu::new("LspLogView")
1083 .anchor(AnchorCorner::TopLeft)
1084 .trigger(Button::new(
1085 "language_server_menu_header",
1086 current_server
1087 .map(|row| {
1088 Cow::Owned(format!(
1089 "{} ({}) - {}",
1090 row.server_name.0,
1091 row.worktree_root_name,
1092 row.selected_entry.label()
1093 ))
1094 })
1095 .unwrap_or_else(|| "No server selected".into()),
1096 ))
1097 .menu({
1098 let log_view = log_view.clone();
1099 move |cx| {
1100 let menu_rows = menu_rows.clone();
1101 let log_view = log_view.clone();
1102 let log_toolbar_view = log_toolbar_view.clone();
1103 ContextMenu::build(cx, move |mut menu, cx| {
1104 for (ix, row) in menu_rows.into_iter().enumerate() {
1105 let server_selected = Some(row.server_id) == current_server_id;
1106 menu = menu
1107 .header(format!(
1108 "{} ({})",
1109 row.server_name.0, row.worktree_root_name
1110 ))
1111 .entry(
1112 SERVER_LOGS,
1113 None,
1114 cx.handler_for(&log_view, move |view, cx| {
1115 view.show_logs_for_server(row.server_id, cx);
1116 }),
1117 );
1118 // We do not support tracing for remote language servers right now
1119 if row.server_kind.is_remote() {
1120 return menu;
1121 }
1122 menu = menu.entry(
1123 SERVER_TRACE,
1124 None,
1125 cx.handler_for(&log_view, move |view, cx| {
1126 view.show_trace_for_server(row.server_id, cx);
1127 }),
1128 );
1129 menu = menu.custom_entry(
1130 {
1131 let log_toolbar_view = log_toolbar_view.clone();
1132 move |cx| {
1133 h_flex()
1134 .w_full()
1135 .justify_between()
1136 .child(Label::new(RPC_MESSAGES))
1137 .child(
1138 div().child(
1139 Checkbox::new(
1140 ix,
1141 if row.rpc_trace_enabled {
1142 Selection::Selected
1143 } else {
1144 Selection::Unselected
1145 },
1146 )
1147 .on_click(cx.listener_for(
1148 &log_toolbar_view,
1149 move |view, selection, cx| {
1150 let enabled = matches!(
1151 selection,
1152 Selection::Selected
1153 );
1154 view.toggle_rpc_logging_for_server(
1155 row.server_id,
1156 enabled,
1157 cx,
1158 );
1159 cx.stop_propagation();
1160 },
1161 )),
1162 ),
1163 )
1164 .into_any_element()
1165 }
1166 },
1167 cx.handler_for(&log_view, move |view, cx| {
1168 view.show_rpc_trace_for_server(row.server_id, cx);
1169 }),
1170 );
1171 if server_selected && row.selected_entry == LogKind::Rpc {
1172 let selected_ix = menu.select_last();
1173 debug_assert_eq!(
1174 Some(ix * 4 + 3),
1175 selected_ix,
1176 "Could not scroll to a just added LSP menu item"
1177 );
1178 }
1179 }
1180 menu
1181 })
1182 .into()
1183 }
1184 });
1185
1186 h_flex()
1187 .size_full()
1188 .child(lsp_menu)
1189 .child(
1190 div()
1191 .child(
1192 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1193 |this, _, cx| {
1194 if let Some(log_view) = this.log_view.as_ref() {
1195 log_view.update(cx, |log_view, cx| {
1196 log_view.editor.update(cx, |editor, cx| {
1197 editor.set_read_only(false);
1198 editor.clear(cx);
1199 editor.set_read_only(true);
1200 });
1201 })
1202 }
1203 },
1204 )),
1205 )
1206 .ml_2(),
1207 )
1208 .child(log_view.update(cx, |this, _| match this.active_entry_kind {
1209 LogKind::Trace => {
1210 let log_view = log_view.clone();
1211 div().child(
1212 PopoverMenu::new("lsp-trace-level-menu")
1213 .anchor(AnchorCorner::TopLeft)
1214 .trigger(Button::new(
1215 "language_server_trace_level_selector",
1216 "Trace level",
1217 ))
1218 .menu({
1219 let log_view = log_view.clone();
1220
1221 move |cx| {
1222 let id = log_view.read(cx).current_server_id?;
1223
1224 let trace_level = log_view.update(cx, |this, cx| {
1225 this.log_store.update(cx, |this, _| {
1226 Some(this.get_language_server_state(id)?.trace_level)
1227 })
1228 })?;
1229
1230 ContextMenu::build(cx, |mut menu, _| {
1231 let log_view = log_view.clone();
1232
1233 for (option, label) in [
1234 (TraceValue::Off, "Off"),
1235 (TraceValue::Messages, "Messages"),
1236 (TraceValue::Verbose, "Verbose"),
1237 ] {
1238 menu = menu.entry(label, None, {
1239 let log_view = log_view.clone();
1240 move |cx| {
1241 log_view.update(cx, |this, cx| {
1242 if let Some(id) = this.current_server_id {
1243 this.update_trace_level(id, option, cx);
1244 }
1245 });
1246 }
1247 });
1248 if option == trace_level {
1249 menu.select_last();
1250 }
1251 }
1252
1253 menu
1254 })
1255 .into()
1256 }
1257 }),
1258 )
1259 }
1260 LogKind::Logs => {
1261 let log_view = log_view.clone();
1262 div().child(
1263 PopoverMenu::new("lsp-log-level-menu")
1264 .anchor(AnchorCorner::TopLeft)
1265 .trigger(Button::new(
1266 "language_server_log_level_selector",
1267 "Log level",
1268 ))
1269 .menu({
1270 let log_view = log_view.clone();
1271
1272 move |cx| {
1273 let id = log_view.read(cx).current_server_id?;
1274
1275 let log_level = log_view.update(cx, |this, cx| {
1276 this.log_store.update(cx, |this, _| {
1277 Some(this.get_language_server_state(id)?.log_level)
1278 })
1279 })?;
1280
1281 ContextMenu::build(cx, |mut menu, _| {
1282 let log_view = log_view.clone();
1283
1284 for (option, label) in [
1285 (MessageType::LOG, "Log"),
1286 (MessageType::INFO, "Info"),
1287 (MessageType::WARNING, "Warning"),
1288 (MessageType::ERROR, "Error"),
1289 ] {
1290 menu = menu.entry(label, None, {
1291 let log_view = log_view.clone();
1292 move |cx| {
1293 log_view.update(cx, |this, cx| {
1294 if let Some(id) = this.current_server_id {
1295 this.update_log_level(id, option, cx);
1296 }
1297 });
1298 }
1299 });
1300 if option == log_level {
1301 menu.select_last();
1302 }
1303 }
1304
1305 menu
1306 })
1307 .into()
1308 }
1309 }),
1310 )
1311 }
1312 _ => div(),
1313 }))
1314 }
1315}
1316
1317const RPC_MESSAGES: &str = "RPC Messages";
1318const SERVER_LOGS: &str = "Server Logs";
1319const SERVER_TRACE: &str = "Server Trace";
1320
1321impl Default for LspLogToolbarItemView {
1322 fn default() -> Self {
1323 Self::new()
1324 }
1325}
1326
1327impl LspLogToolbarItemView {
1328 pub fn new() -> Self {
1329 Self {
1330 log_view: None,
1331 _log_view_subscription: None,
1332 }
1333 }
1334
1335 fn toggle_rpc_logging_for_server(
1336 &mut self,
1337 id: LanguageServerId,
1338 enabled: bool,
1339 cx: &mut ViewContext<Self>,
1340 ) {
1341 if let Some(log_view) = &self.log_view {
1342 log_view.update(cx, |log_view, cx| {
1343 log_view.toggle_rpc_trace_for_server(id, enabled, cx);
1344 if !enabled && Some(id) == log_view.current_server_id {
1345 log_view.show_logs_for_server(id, cx);
1346 cx.notify();
1347 } else if enabled {
1348 log_view.show_rpc_trace_for_server(id, cx);
1349 cx.notify();
1350 }
1351 cx.focus(&log_view.focus_handle);
1352 });
1353 }
1354 cx.notify();
1355 }
1356}
1357
1358pub enum Event {
1359 NewServerLogEntry {
1360 id: LanguageServerId,
1361 entry: String,
1362 kind: LogKind,
1363 },
1364}
1365
1366impl EventEmitter<Event> for LogStore {}
1367impl EventEmitter<Event> for LspLogView {}
1368impl EventEmitter<EditorEvent> for LspLogView {}
1369impl EventEmitter<SearchEvent> for LspLogView {}