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