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