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