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