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