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