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