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, ServerCapabilities,
13 SetTraceParams, TraceValue,
14};
15use project::{search::SearchQuery, Project, WorktreeId};
16use std::{borrow::Cow, sync::Arc};
17use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
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 .filter_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 let state = log_store.language_servers.get(&server_id)?;
763 Some(LogMenuItem {
764 server_id: *server_id,
765 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
766 server_kind: state.kind.clone(),
767 worktree_root_name,
768 rpc_trace_enabled: state.rpc_state.is_some(),
769 selected_entry: self.active_entry_kind,
770 trace_level: lsp::TraceValue::Off,
771 })
772 }
773
774 LanguageServerKind::Global => Some(LogMenuItem {
775 server_id: *server_id,
776 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
777 server_kind: state.kind.clone(),
778 worktree_root_name: "supplementary".to_string(),
779 rpc_trace_enabled: state.rpc_state.is_some(),
780 selected_entry: self.active_entry_kind,
781 trace_level: lsp::TraceValue::Off,
782 }),
783 })
784 .chain(
785 self.project
786 .read(cx)
787 .supplementary_language_servers(cx)
788 .filter_map(|(server_id, name)| {
789 let state = log_store.language_servers.get(&server_id)?;
790 Some(LogMenuItem {
791 server_id,
792 server_name: name.clone(),
793 server_kind: state.kind.clone(),
794 worktree_root_name: "supplementary".to_string(),
795 rpc_trace_enabled: state.rpc_state.is_some(),
796 selected_entry: self.active_entry_kind,
797 trace_level: lsp::TraceValue::Off,
798 })
799 }),
800 )
801 .collect::<Vec<_>>();
802 rows.sort_by_key(|row| row.server_id);
803 rows.dedup_by_key(|row| row.server_id);
804 Some(rows)
805 }
806
807 fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
808 let typ = self
809 .log_store
810 .read_with(cx, |v, _| {
811 v.language_servers.get(&server_id).map(|v| v.log_level)
812 })
813 .unwrap_or(MessageType::LOG);
814 let log_contents = self
815 .log_store
816 .read(cx)
817 .server_logs(server_id)
818 .map(|v| log_contents(v, typ));
819 if let Some(log_contents) = log_contents {
820 self.current_server_id = Some(server_id);
821 self.active_entry_kind = LogKind::Logs;
822 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
823 self.editor = editor;
824 self.editor_subscriptions = editor_subscriptions;
825 cx.notify();
826 }
827 cx.focus(&self.focus_handle);
828 }
829
830 fn update_log_level(
831 &self,
832 server_id: LanguageServerId,
833 level: MessageType,
834 cx: &mut ViewContext<Self>,
835 ) {
836 let log_contents = self.log_store.update(cx, |this, _| {
837 if let Some(state) = this.get_language_server_state(server_id) {
838 state.log_level = level;
839 }
840
841 this.server_logs(server_id).map(|v| log_contents(v, level))
842 });
843
844 if let Some(log_contents) = log_contents {
845 self.editor.update(cx, move |editor, cx| {
846 editor.set_text(log_contents, cx);
847 editor.move_to_end(&MoveToEnd, cx);
848 });
849 cx.notify();
850 }
851
852 cx.focus(&self.focus_handle);
853 }
854
855 fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
856 let log_contents = self
857 .log_store
858 .read(cx)
859 .server_trace(server_id)
860 .map(|v| log_contents(v, ()));
861 if let Some(log_contents) = log_contents {
862 self.current_server_id = Some(server_id);
863 self.active_entry_kind = LogKind::Trace;
864 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
865 self.editor = editor;
866 self.editor_subscriptions = editor_subscriptions;
867 cx.notify();
868 }
869 cx.focus(&self.focus_handle);
870 }
871
872 fn show_rpc_trace_for_server(
873 &mut self,
874 server_id: LanguageServerId,
875 cx: &mut ViewContext<Self>,
876 ) {
877 let rpc_log = self.log_store.update(cx, |log_store, _| {
878 log_store
879 .enable_rpc_trace_for_language_server(server_id)
880 .map(|state| log_contents(&state.rpc_messages, ()))
881 });
882 if let Some(rpc_log) = rpc_log {
883 self.current_server_id = Some(server_id);
884 self.active_entry_kind = LogKind::Rpc;
885 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx);
886 let language = self.project.read(cx).languages().language_for_name("JSON");
887 editor
888 .read(cx)
889 .buffer()
890 .read(cx)
891 .as_singleton()
892 .expect("log buffer should be a singleton")
893 .update(cx, |_, cx| {
894 cx.spawn({
895 let buffer = cx.handle();
896 |_, mut cx| async move {
897 let language = language.await.ok();
898 buffer.update(&mut cx, |buffer, cx| {
899 buffer.set_language(language, cx);
900 })
901 }
902 })
903 .detach_and_log_err(cx);
904 });
905
906 self.editor = editor;
907 self.editor_subscriptions = editor_subscriptions;
908 cx.notify();
909 }
910
911 cx.focus(&self.focus_handle);
912 }
913
914 fn toggle_rpc_trace_for_server(
915 &mut self,
916 server_id: LanguageServerId,
917 enabled: bool,
918 cx: &mut ViewContext<Self>,
919 ) {
920 self.log_store.update(cx, |log_store, _| {
921 if enabled {
922 log_store.enable_rpc_trace_for_language_server(server_id);
923 } else {
924 log_store.disable_rpc_trace_for_language_server(server_id);
925 }
926 });
927 if !enabled && Some(server_id) == self.current_server_id {
928 self.show_logs_for_server(server_id, cx);
929 cx.notify();
930 }
931 }
932
933 fn update_trace_level(
934 &self,
935 server_id: LanguageServerId,
936 level: TraceValue,
937 cx: &mut ViewContext<Self>,
938 ) {
939 if let Some(server) = self.project.read(cx).language_server_for_id(server_id, cx) {
940 self.log_store.update(cx, |this, _| {
941 if let Some(state) = this.get_language_server_state(server_id) {
942 state.trace_level = level;
943 }
944 });
945
946 server
947 .notify::<SetTrace>(SetTraceParams { value: level })
948 .ok();
949 }
950 }
951
952 fn show_capabilities_for_server(
953 &mut self,
954 server_id: LanguageServerId,
955 cx: &mut ViewContext<Self>,
956 ) {
957 let capabilities = self.log_store.read(cx).server_capabilities(server_id);
958
959 if let Some(capabilities) = capabilities {
960 self.current_server_id = Some(server_id);
961 self.active_entry_kind = LogKind::Capabilities;
962 let (editor, editor_subscriptions) =
963 Self::editor_for_capabilities(capabilities.clone(), cx);
964 self.editor = editor;
965 self.editor_subscriptions = editor_subscriptions;
966 cx.notify();
967 }
968 cx.focus(&self.focus_handle);
969 }
970}
971
972fn log_filter<T: Message>(line: &T, cmp: <T as Message>::Level) -> Option<&str> {
973 if line.should_include(cmp) {
974 Some(line.as_ref())
975 } else {
976 None
977 }
978}
979
980fn log_contents<T: Message>(lines: &VecDeque<T>, cmp: <T as Message>::Level) -> String {
981 let (a, b) = lines.as_slices();
982 let a = a.iter().filter_map(move |v| log_filter(v, cmp));
983 let b = b.iter().filter_map(move |v| log_filter(v, cmp));
984 a.chain(b).fold(String::new(), |mut acc, el| {
985 acc.push_str(el);
986 acc.push('\n');
987 acc
988 })
989}
990
991impl Render for LspLogView {
992 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
993 self.editor
994 .update(cx, |editor, cx| editor.render(cx).into_any_element())
995 }
996}
997
998impl FocusableView for LspLogView {
999 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
1000 self.focus_handle.clone()
1001 }
1002}
1003
1004impl Item for LspLogView {
1005 type Event = EditorEvent;
1006
1007 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1008 Editor::to_item_events(event, f)
1009 }
1010
1011 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
1012 Some("LSP Logs".into())
1013 }
1014
1015 fn telemetry_event_text(&self) -> Option<&'static str> {
1016 None
1017 }
1018
1019 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1020 Some(Box::new(handle.clone()))
1021 }
1022
1023 fn clone_on_split(
1024 &self,
1025 _workspace_id: Option<WorkspaceId>,
1026 cx: &mut ViewContext<Self>,
1027 ) -> Option<View<Self>>
1028 where
1029 Self: Sized,
1030 {
1031 Some(cx.new_view(|cx| {
1032 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), cx);
1033 if let Some(server_id) = self.current_server_id {
1034 match self.active_entry_kind {
1035 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, cx),
1036 LogKind::Trace => new_view.show_trace_for_server(server_id, cx),
1037 LogKind::Logs => new_view.show_logs_for_server(server_id, cx),
1038 LogKind::Capabilities => new_view.show_capabilities_for_server(server_id, cx),
1039 }
1040 }
1041 new_view
1042 }))
1043 }
1044}
1045
1046impl SearchableItem for LspLogView {
1047 type Match = <Editor as SearchableItem>::Match;
1048
1049 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
1050 self.editor.update(cx, |e, cx| e.clear_matches(cx))
1051 }
1052
1053 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1054 self.editor
1055 .update(cx, |e, cx| e.update_matches(matches, cx))
1056 }
1057
1058 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
1059 self.editor.update(cx, |e, cx| e.query_suggestion(cx))
1060 }
1061
1062 fn activate_match(
1063 &mut self,
1064 index: usize,
1065 matches: &[Self::Match],
1066 cx: &mut ViewContext<Self>,
1067 ) {
1068 self.editor
1069 .update(cx, |e, cx| e.activate_match(index, matches, cx))
1070 }
1071
1072 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1073 self.editor
1074 .update(cx, |e, cx| e.select_matches(matches, cx))
1075 }
1076
1077 fn find_matches(
1078 &mut self,
1079 query: Arc<project::search::SearchQuery>,
1080 cx: &mut ViewContext<Self>,
1081 ) -> gpui::Task<Vec<Self::Match>> {
1082 self.editor.update(cx, |e, cx| e.find_matches(query, cx))
1083 }
1084
1085 fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
1086 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1087 }
1088 fn supported_options() -> workspace::searchable::SearchOptions {
1089 workspace::searchable::SearchOptions {
1090 case: true,
1091 word: true,
1092 regex: true,
1093 // LSP log is read-only.
1094 replacement: false,
1095 selection: false,
1096 }
1097 }
1098 fn active_match_index(
1099 &mut self,
1100 matches: &[Self::Match],
1101 cx: &mut ViewContext<Self>,
1102 ) -> Option<usize> {
1103 self.editor
1104 .update(cx, |e, cx| e.active_match_index(matches, cx))
1105 }
1106}
1107
1108impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1109
1110impl ToolbarItemView for LspLogToolbarItemView {
1111 fn set_active_pane_item(
1112 &mut self,
1113 active_pane_item: Option<&dyn ItemHandle>,
1114 cx: &mut ViewContext<Self>,
1115 ) -> workspace::ToolbarItemLocation {
1116 if let Some(item) = active_pane_item {
1117 if let Some(log_view) = item.downcast::<LspLogView>() {
1118 self.log_view = Some(log_view.clone());
1119 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1120 cx.notify();
1121 }));
1122 return ToolbarItemLocation::PrimaryLeft;
1123 }
1124 }
1125 self.log_view = None;
1126 self._log_view_subscription = None;
1127 ToolbarItemLocation::Hidden
1128 }
1129}
1130
1131impl Render for LspLogToolbarItemView {
1132 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1133 let Some(log_view) = self.log_view.clone() else {
1134 return div();
1135 };
1136 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1137 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1138 let current_server_id = log_view.current_server_id;
1139 (menu_rows, current_server_id)
1140 });
1141
1142 let current_server = current_server_id.and_then(|current_server_id| {
1143 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1144 Some(menu_rows[ix].clone())
1145 } else {
1146 None
1147 }
1148 });
1149
1150 let log_toolbar_view = cx.view().clone();
1151 let lsp_menu = PopoverMenu::new("LspLogView")
1152 .anchor(AnchorCorner::TopLeft)
1153 .trigger(Button::new(
1154 "language_server_menu_header",
1155 current_server
1156 .map(|row| {
1157 Cow::Owned(format!(
1158 "{} ({}) - {}",
1159 row.server_name.0,
1160 row.worktree_root_name,
1161 row.selected_entry.label()
1162 ))
1163 })
1164 .unwrap_or_else(|| "No server selected".into()),
1165 ))
1166 .menu({
1167 let log_view = log_view.clone();
1168 move |cx| {
1169 let menu_rows = menu_rows.clone();
1170 let log_view = log_view.clone();
1171 let log_toolbar_view = log_toolbar_view.clone();
1172 ContextMenu::build(cx, move |mut menu, cx| {
1173 for (ix, row) in menu_rows.into_iter().enumerate() {
1174 let server_selected = Some(row.server_id) == current_server_id;
1175 menu = menu
1176 .header(format!(
1177 "{} ({})",
1178 row.server_name.0, row.worktree_root_name
1179 ))
1180 .entry(
1181 SERVER_LOGS,
1182 None,
1183 cx.handler_for(&log_view, move |view, cx| {
1184 view.show_logs_for_server(row.server_id, cx);
1185 }),
1186 );
1187 // We do not support tracing for remote language servers right now
1188 if row.server_kind.is_remote() {
1189 return menu;
1190 }
1191 menu = menu.entry(
1192 SERVER_TRACE,
1193 None,
1194 cx.handler_for(&log_view, move |view, cx| {
1195 view.show_trace_for_server(row.server_id, cx);
1196 }),
1197 );
1198 menu = menu.custom_entry(
1199 {
1200 let log_toolbar_view = log_toolbar_view.clone();
1201 move |cx| {
1202 h_flex()
1203 .w_full()
1204 .justify_between()
1205 .child(Label::new(RPC_MESSAGES))
1206 .child(
1207 div().child(
1208 Checkbox::new(
1209 ix,
1210 if row.rpc_trace_enabled {
1211 Selection::Selected
1212 } else {
1213 Selection::Unselected
1214 },
1215 )
1216 .on_click(cx.listener_for(
1217 &log_toolbar_view,
1218 move |view, selection, cx| {
1219 let enabled = matches!(
1220 selection,
1221 Selection::Selected
1222 );
1223 view.toggle_rpc_logging_for_server(
1224 row.server_id,
1225 enabled,
1226 cx,
1227 );
1228 cx.stop_propagation();
1229 },
1230 )),
1231 ),
1232 )
1233 .into_any_element()
1234 }
1235 },
1236 cx.handler_for(&log_view, move |view, cx| {
1237 view.show_rpc_trace_for_server(row.server_id, cx);
1238 }),
1239 );
1240 if server_selected && row.selected_entry == LogKind::Rpc {
1241 let selected_ix = menu.select_last();
1242 // Each language server has:
1243 // 1. A title.
1244 // 2. Server logs.
1245 // 3. Server trace.
1246 // 4. RPC messages.
1247 // 5. Server capabilities
1248 // Thus, if nth server's RPC is selected, the index of selected entry should match this formula
1249 let _expected_index = ix * 5 + 3;
1250 debug_assert_eq!(
1251 Some(_expected_index),
1252 selected_ix,
1253 "Could not scroll to a just added LSP menu item"
1254 );
1255 }
1256 menu = menu.entry(
1257 SERVER_CAPABILITIES,
1258 None,
1259 cx.handler_for(&log_view, move |view, cx| {
1260 view.show_capabilities_for_server(row.server_id, cx);
1261 }),
1262 );
1263 }
1264 menu
1265 })
1266 .into()
1267 }
1268 });
1269
1270 h_flex()
1271 .size_full()
1272 .child(lsp_menu)
1273 .child(
1274 div()
1275 .child(
1276 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1277 |this, _, cx| {
1278 if let Some(log_view) = this.log_view.as_ref() {
1279 log_view.update(cx, |log_view, cx| {
1280 log_view.editor.update(cx, |editor, cx| {
1281 editor.set_read_only(false);
1282 editor.clear(cx);
1283 editor.set_read_only(true);
1284 });
1285 })
1286 }
1287 },
1288 )),
1289 )
1290 .ml_2(),
1291 )
1292 .child(log_view.update(cx, |this, _| match this.active_entry_kind {
1293 LogKind::Trace => {
1294 let log_view = log_view.clone();
1295 div().child(
1296 PopoverMenu::new("lsp-trace-level-menu")
1297 .anchor(AnchorCorner::TopLeft)
1298 .trigger(Button::new(
1299 "language_server_trace_level_selector",
1300 "Trace level",
1301 ))
1302 .menu({
1303 let log_view = log_view.clone();
1304
1305 move |cx| {
1306 let id = log_view.read(cx).current_server_id?;
1307
1308 let trace_level = log_view.update(cx, |this, cx| {
1309 this.log_store.update(cx, |this, _| {
1310 Some(this.get_language_server_state(id)?.trace_level)
1311 })
1312 })?;
1313
1314 ContextMenu::build(cx, |mut menu, _| {
1315 let log_view = log_view.clone();
1316
1317 for (option, label) in [
1318 (TraceValue::Off, "Off"),
1319 (TraceValue::Messages, "Messages"),
1320 (TraceValue::Verbose, "Verbose"),
1321 ] {
1322 menu = menu.entry(label, None, {
1323 let log_view = log_view.clone();
1324 move |cx| {
1325 log_view.update(cx, |this, cx| {
1326 if let Some(id) = this.current_server_id {
1327 this.update_trace_level(id, option, cx);
1328 }
1329 });
1330 }
1331 });
1332 if option == trace_level {
1333 menu.select_last();
1334 }
1335 }
1336
1337 menu
1338 })
1339 .into()
1340 }
1341 }),
1342 )
1343 }
1344 LogKind::Logs => {
1345 let log_view = log_view.clone();
1346 div().child(
1347 PopoverMenu::new("lsp-log-level-menu")
1348 .anchor(AnchorCorner::TopLeft)
1349 .trigger(Button::new(
1350 "language_server_log_level_selector",
1351 "Log level",
1352 ))
1353 .menu({
1354 let log_view = log_view.clone();
1355
1356 move |cx| {
1357 let id = log_view.read(cx).current_server_id?;
1358
1359 let log_level = log_view.update(cx, |this, cx| {
1360 this.log_store.update(cx, |this, _| {
1361 Some(this.get_language_server_state(id)?.log_level)
1362 })
1363 })?;
1364
1365 ContextMenu::build(cx, |mut menu, _| {
1366 let log_view = log_view.clone();
1367
1368 for (option, label) in [
1369 (MessageType::LOG, "Log"),
1370 (MessageType::INFO, "Info"),
1371 (MessageType::WARNING, "Warning"),
1372 (MessageType::ERROR, "Error"),
1373 ] {
1374 menu = menu.entry(label, None, {
1375 let log_view = log_view.clone();
1376 move |cx| {
1377 log_view.update(cx, |this, cx| {
1378 if let Some(id) = this.current_server_id {
1379 this.update_log_level(id, option, cx);
1380 }
1381 });
1382 }
1383 });
1384 if option == log_level {
1385 menu.select_last();
1386 }
1387 }
1388
1389 menu
1390 })
1391 .into()
1392 }
1393 }),
1394 )
1395 }
1396 _ => div(),
1397 }))
1398 }
1399}
1400
1401const RPC_MESSAGES: &str = "RPC Messages";
1402const SERVER_LOGS: &str = "Server Logs";
1403const SERVER_TRACE: &str = "Server Trace";
1404const SERVER_CAPABILITIES: &str = "Server Capabilities";
1405
1406impl Default for LspLogToolbarItemView {
1407 fn default() -> Self {
1408 Self::new()
1409 }
1410}
1411
1412impl LspLogToolbarItemView {
1413 pub fn new() -> Self {
1414 Self {
1415 log_view: None,
1416 _log_view_subscription: None,
1417 }
1418 }
1419
1420 fn toggle_rpc_logging_for_server(
1421 &mut self,
1422 id: LanguageServerId,
1423 enabled: bool,
1424 cx: &mut ViewContext<Self>,
1425 ) {
1426 if let Some(log_view) = &self.log_view {
1427 log_view.update(cx, |log_view, cx| {
1428 log_view.toggle_rpc_trace_for_server(id, enabled, cx);
1429 if !enabled && Some(id) == log_view.current_server_id {
1430 log_view.show_logs_for_server(id, cx);
1431 cx.notify();
1432 } else if enabled {
1433 log_view.show_rpc_trace_for_server(id, cx);
1434 cx.notify();
1435 }
1436 cx.focus(&log_view.focus_handle);
1437 });
1438 }
1439 cx.notify();
1440 }
1441}
1442
1443pub enum Event {
1444 NewServerLogEntry {
1445 id: LanguageServerId,
1446 entry: String,
1447 kind: LogKind,
1448 },
1449}
1450
1451impl EventEmitter<Event> for LogStore {}
1452impl EventEmitter<Event> for LspLogView {}
1453impl EventEmitter<EditorEvent> for LspLogView {}
1454impl EventEmitter<SearchEvent> for LspLogView {}