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