1#[cfg(test)]
2mod lsp_log_tests;
3
4use collections::HashMap;
5use editor::Editor;
6use futures::{channel::mpsc, StreamExt};
7use gpui::{
8 actions,
9 elements::{
10 AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
11 ParentElement, Stack,
12 },
13 platform::{CursorStyle, MouseButton},
14 AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
15 ViewHandle, WeakModelHandle,
16};
17use language::{Buffer, LanguageServerId, LanguageServerName};
18use project::{Project, Worktree};
19use std::{borrow::Cow, sync::Arc};
20use theme::{ui, Theme};
21use workspace::{
22 item::{Item, ItemHandle},
23 searchable::{SearchableItem, SearchableItemHandle},
24 ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
25};
26
27const SEND_LINE: &str = "// Send:\n";
28const RECEIVE_LINE: &str = "// Receive:\n";
29
30struct LogStore {
31 projects: HashMap<WeakModelHandle<Project>, ProjectState>,
32 io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
33}
34
35struct ProjectState {
36 servers: HashMap<LanguageServerId, LanguageServerState>,
37 _subscriptions: [gpui::Subscription; 2],
38}
39
40struct LanguageServerState {
41 log_buffer: ModelHandle<Buffer>,
42 rpc_state: Option<LanguageServerRpcState>,
43}
44
45struct LanguageServerRpcState {
46 buffer: ModelHandle<Buffer>,
47 last_message_kind: Option<MessageKind>,
48 _subscription: lsp::Subscription,
49}
50
51pub struct LspLogView {
52 log_store: ModelHandle<LogStore>,
53 current_server_id: Option<LanguageServerId>,
54 is_showing_rpc_trace: bool,
55 editor: ViewHandle<Editor>,
56 project: ModelHandle<Project>,
57}
58
59pub struct LspLogToolbarItemView {
60 log_view: Option<ViewHandle<LspLogView>>,
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)]
71struct LogMenuItem {
72 server_id: LanguageServerId,
73 server_name: LanguageServerName,
74 worktree: ModelHandle<Worktree>,
75 rpc_trace_enabled: bool,
76 rpc_trace_selected: bool,
77 logs_selected: bool,
78}
79
80actions!(log, [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 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, is_output, 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, is_output, &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 Some(
175 project_state
176 .servers
177 .entry(id)
178 .or_insert_with(|| {
179 cx.notify();
180 LanguageServerState {
181 rpc_state: None,
182 log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
183 }
184 })
185 .log_buffer
186 .clone(),
187 )
188 }
189
190 fn add_language_server_log(
191 &mut self,
192 project: &ModelHandle<Project>,
193 id: LanguageServerId,
194 message: &str,
195 cx: &mut ModelContext<Self>,
196 ) -> Option<()> {
197 let buffer = self.add_language_server(&project, id, cx)?;
198 buffer.update(cx, |buffer, cx| {
199 let len = buffer.len();
200 let has_newline = message.ends_with("\n");
201 buffer.edit([(len..len, message)], None, cx);
202 if !has_newline {
203 let len = buffer.len();
204 buffer.edit([(len..len, "\n")], None, cx);
205 }
206 });
207 cx.notify();
208 Some(())
209 }
210
211 fn remove_language_server(
212 &mut self,
213 project: &ModelHandle<Project>,
214 id: LanguageServerId,
215 cx: &mut ModelContext<Self>,
216 ) -> Option<()> {
217 let project_state = self.projects.get_mut(&project.downgrade())?;
218 project_state.servers.remove(&id);
219 cx.notify();
220 Some(())
221 }
222
223 pub fn log_buffer_for_server(
224 &self,
225 project: &ModelHandle<Project>,
226 server_id: LanguageServerId,
227 ) -> Option<ModelHandle<Buffer>> {
228 let weak_project = project.downgrade();
229 let project_state = self.projects.get(&weak_project)?;
230 let server_state = project_state.servers.get(&server_id)?;
231 Some(server_state.log_buffer.clone())
232 }
233
234 pub fn enable_rpc_trace_for_language_server(
235 &mut self,
236 project: &ModelHandle<Project>,
237 server_id: LanguageServerId,
238 cx: &mut ModelContext<Self>,
239 ) -> Option<ModelHandle<Buffer>> {
240 let weak_project = project.downgrade();
241 let project_state = self.projects.get_mut(&weak_project)?;
242 let server_state = project_state.servers.get_mut(&server_id)?;
243 let server = project.read(cx).language_server_for_id(server_id)?;
244 let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
245 let io_tx = self.io_tx.clone();
246 let language = project.read(cx).languages().language_for_name("JSON");
247 let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
248 cx.spawn_weak({
249 let buffer = buffer.clone();
250 |_, mut cx| async move {
251 let language = language.await.ok();
252 buffer.update(&mut cx, |buffer, cx| {
253 buffer.set_language(language, cx);
254 });
255 }
256 })
257 .detach();
258
259 LanguageServerRpcState {
260 buffer,
261 last_message_kind: None,
262 _subscription: server.on_io(move |is_received, json| {
263 io_tx
264 .unbounded_send((weak_project, server_id, is_received, json.to_string()))
265 .ok();
266 }),
267 }
268 });
269 Some(rpc_state.buffer.clone())
270 }
271
272 pub fn disable_rpc_trace_for_language_server(
273 &mut self,
274 project: &ModelHandle<Project>,
275 server_id: LanguageServerId,
276 _: &mut ModelContext<Self>,
277 ) -> Option<()> {
278 let project = project.downgrade();
279 let project_state = self.projects.get_mut(&project)?;
280 let server_state = project_state.servers.get_mut(&server_id)?;
281 server_state.rpc_state.take();
282 Some(())
283 }
284
285 fn on_io(
286 &mut self,
287 project: WeakModelHandle<Project>,
288 language_server_id: LanguageServerId,
289 is_received: bool,
290 message: &str,
291 cx: &mut AppContext,
292 ) -> Option<()> {
293 let state = self
294 .projects
295 .get_mut(&project)?
296 .servers
297 .get_mut(&language_server_id)?
298 .rpc_state
299 .as_mut()?;
300 state.buffer.update(cx, |buffer, cx| {
301 let kind = if is_received {
302 MessageKind::Receive
303 } else {
304 MessageKind::Send
305 };
306 if state.last_message_kind != Some(kind) {
307 let len = buffer.len();
308 let line = match kind {
309 MessageKind::Send => SEND_LINE,
310 MessageKind::Receive => RECEIVE_LINE,
311 };
312 buffer.edit([(len..len, line)], None, cx);
313 state.last_message_kind = Some(kind);
314 }
315 let len = buffer.len();
316 buffer.edit([(len..len, message)], None, cx);
317 });
318 Some(())
319 }
320}
321
322impl LspLogView {
323 fn new(
324 project: ModelHandle<Project>,
325 log_store: ModelHandle<LogStore>,
326 cx: &mut ViewContext<Self>,
327 ) -> Self {
328 let server_id = log_store
329 .read(cx)
330 .projects
331 .get(&project.downgrade())
332 .and_then(|project| project.servers.keys().copied().next());
333 let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
334 let mut this = Self {
335 editor: Self::editor_for_buffer(project.clone(), buffer, cx),
336 project,
337 log_store,
338 current_server_id: None,
339 is_showing_rpc_trace: false,
340 };
341 if let Some(server_id) = server_id {
342 this.show_logs_for_server(server_id, cx);
343 }
344 this
345 }
346
347 fn editor_for_buffer(
348 project: ModelHandle<Project>,
349 buffer: ModelHandle<Buffer>,
350 cx: &mut ViewContext<Self>,
351 ) -> ViewHandle<Editor> {
352 let editor = cx.add_view(|cx| {
353 let mut editor = Editor::for_buffer(buffer, Some(project), cx);
354 editor.set_read_only(true);
355 editor.move_to_end(&Default::default(), cx);
356 editor
357 });
358 cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
359 .detach();
360 editor
361 }
362
363 fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
364 let log_store = self.log_store.read(cx);
365 let state = log_store.projects.get(&self.project.downgrade())?;
366 let mut rows = self
367 .project
368 .read(cx)
369 .language_servers()
370 .filter_map(|(server_id, language_server_name, worktree_id)| {
371 let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
372 let state = state.servers.get(&server_id)?;
373 Some(LogMenuItem {
374 server_id,
375 server_name: language_server_name,
376 worktree,
377 rpc_trace_enabled: state.rpc_state.is_some(),
378 rpc_trace_selected: self.is_showing_rpc_trace
379 && self.current_server_id == Some(server_id),
380 logs_selected: !self.is_showing_rpc_trace
381 && self.current_server_id == Some(server_id),
382 })
383 })
384 .collect::<Vec<_>>();
385 rows.sort_by_key(|row| row.server_id);
386 rows.dedup_by_key(|row| row.server_id);
387 Some(rows)
388 }
389
390 fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
391 let buffer = self
392 .log_store
393 .read(cx)
394 .log_buffer_for_server(&self.project, server_id);
395 if let Some(buffer) = buffer {
396 self.current_server_id = Some(server_id);
397 self.is_showing_rpc_trace = false;
398 self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
399 cx.notify();
400 }
401 }
402
403 fn show_rpc_trace_for_server(
404 &mut self,
405 server_id: LanguageServerId,
406 cx: &mut ViewContext<Self>,
407 ) {
408 let buffer = self.log_store.update(cx, |log_set, cx| {
409 log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
410 });
411 if let Some(buffer) = buffer {
412 self.current_server_id = Some(server_id);
413 self.is_showing_rpc_trace = true;
414 self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
415 cx.notify();
416 }
417 }
418
419 fn toggle_rpc_trace_for_server(
420 &mut self,
421 server_id: LanguageServerId,
422 enabled: bool,
423 cx: &mut ViewContext<Self>,
424 ) {
425 self.log_store.update(cx, |log_store, cx| {
426 if enabled {
427 log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
428 } else {
429 log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
430 }
431 });
432 if !enabled && Some(server_id) == self.current_server_id {
433 self.show_logs_for_server(server_id, cx);
434 cx.notify();
435 }
436 }
437}
438
439impl View for LspLogView {
440 fn ui_name() -> &'static str {
441 "LspLogView"
442 }
443
444 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
445 ChildView::new(&self.editor, cx).into_any()
446 }
447
448 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
449 if cx.is_self_focused() {
450 cx.focus(&self.editor);
451 }
452 }
453}
454
455impl Item for LspLogView {
456 fn tab_content<V: View>(
457 &self,
458 _: Option<usize>,
459 style: &theme::Tab,
460 _: &AppContext,
461 ) -> AnyElement<V> {
462 Label::new("LSP Logs", style.label.clone()).into_any()
463 }
464
465 fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
466 Some(Box::new(handle.clone()))
467 }
468}
469
470impl SearchableItem for LspLogView {
471 type Match = <Editor as SearchableItem>::Match;
472
473 fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
474 Editor::to_search_event(event)
475 }
476
477 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
478 self.editor.update(cx, |e, cx| e.clear_matches(cx))
479 }
480
481 fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
482 self.editor
483 .update(cx, |e, cx| e.update_matches(matches, cx))
484 }
485
486 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
487 self.editor.update(cx, |e, cx| e.query_suggestion(cx))
488 }
489
490 fn activate_match(
491 &mut self,
492 index: usize,
493 matches: Vec<Self::Match>,
494 cx: &mut ViewContext<Self>,
495 ) {
496 self.editor
497 .update(cx, |e, cx| e.activate_match(index, matches, cx))
498 }
499
500 fn find_matches(
501 &mut self,
502 query: project::search::SearchQuery,
503 cx: &mut ViewContext<Self>,
504 ) -> gpui::Task<Vec<Self::Match>> {
505 self.editor.update(cx, |e, cx| e.find_matches(query, cx))
506 }
507
508 fn active_match_index(
509 &mut self,
510 matches: Vec<Self::Match>,
511 cx: &mut ViewContext<Self>,
512 ) -> Option<usize> {
513 self.editor
514 .update(cx, |e, cx| e.active_match_index(matches, cx))
515 }
516}
517
518impl ToolbarItemView for LspLogToolbarItemView {
519 fn set_active_pane_item(
520 &mut self,
521 active_pane_item: Option<&dyn ItemHandle>,
522 _: &mut ViewContext<Self>,
523 ) -> workspace::ToolbarItemLocation {
524 self.menu_open = false;
525 if let Some(item) = active_pane_item {
526 if let Some(log_view) = item.downcast::<LspLogView>() {
527 self.log_view = Some(log_view.clone());
528 return ToolbarItemLocation::PrimaryLeft {
529 flex: Some((1., false)),
530 };
531 }
532 }
533 self.log_view = None;
534 ToolbarItemLocation::Hidden
535 }
536}
537
538impl View for LspLogToolbarItemView {
539 fn ui_name() -> &'static str {
540 "LspLogView"
541 }
542
543 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
544 let theme = theme::current(cx).clone();
545 let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
546 let log_view = log_view.read(cx);
547
548 let menu_rows = self
549 .log_view
550 .as_ref()
551 .and_then(|view| view.read(cx).menu_items(cx))
552 .unwrap_or_default();
553
554 let current_server_id = log_view.current_server_id;
555 let current_server = current_server_id.and_then(|current_server_id| {
556 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
557 Some(menu_rows[ix].clone())
558 } else {
559 None
560 }
561 });
562
563 enum Menu {}
564
565 Stack::new()
566 .with_child(Self::render_language_server_menu_header(
567 current_server,
568 &theme,
569 cx,
570 ))
571 .with_children(if self.menu_open {
572 Some(
573 Overlay::new(
574 MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
575 Flex::column()
576 .with_children(menu_rows.into_iter().map(|row| {
577 Self::render_language_server_menu_item(
578 row.server_id,
579 row.server_name,
580 row.worktree,
581 row.rpc_trace_enabled,
582 row.logs_selected,
583 row.rpc_trace_selected,
584 &theme,
585 cx,
586 )
587 }))
588 .contained()
589 .with_style(theme.lsp_log_menu.container)
590 .constrained()
591 .with_width(400.)
592 .with_height(400.)
593 })
594 .on_down_out(MouseButton::Left, |_, this, cx| {
595 this.menu_open = false;
596 cx.notify()
597 }),
598 )
599 .with_fit_mode(OverlayFitMode::SwitchAnchor)
600 .with_anchor_corner(AnchorCorner::TopLeft)
601 .with_z_index(999)
602 .aligned()
603 .bottom()
604 .left(),
605 )
606 } else {
607 None
608 })
609 .aligned()
610 .left()
611 .clipped()
612 .into_any()
613 }
614}
615
616const RPC_MESSAGES: &str = "RPC Messages";
617const SERVER_LOGS: &str = "Server Logs";
618
619impl LspLogToolbarItemView {
620 pub fn new() -> Self {
621 Self {
622 menu_open: false,
623 log_view: None,
624 }
625 }
626
627 fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
628 self.menu_open = !self.menu_open;
629 cx.notify();
630 }
631
632 fn toggle_logging_for_server(
633 &mut self,
634 id: LanguageServerId,
635 enabled: bool,
636 cx: &mut ViewContext<Self>,
637 ) {
638 if let Some(log_view) = &self.log_view {
639 log_view.update(cx, |log_view, cx| {
640 log_view.toggle_rpc_trace_for_server(id, enabled, cx);
641 if !enabled && Some(id) == log_view.current_server_id {
642 log_view.show_logs_for_server(id, cx);
643 cx.notify();
644 }
645 });
646 }
647 cx.notify();
648 }
649
650 fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
651 if let Some(log_view) = &self.log_view {
652 log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
653 self.menu_open = false;
654 cx.notify();
655 }
656 }
657
658 fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
659 if let Some(log_view) = &self.log_view {
660 log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
661 self.menu_open = false;
662 cx.notify();
663 }
664 }
665
666 fn render_language_server_menu_header(
667 current_server: Option<LogMenuItem>,
668 theme: &Arc<Theme>,
669 cx: &mut ViewContext<Self>,
670 ) -> impl Element<Self> {
671 enum ToggleMenu {}
672 MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
673 let label: Cow<str> = current_server
674 .and_then(|row| {
675 let worktree = row.worktree.read(cx);
676 Some(
677 format!(
678 "{} ({}) - {}",
679 row.server_name.0,
680 worktree.root_name(),
681 if row.rpc_trace_selected {
682 RPC_MESSAGES
683 } else {
684 SERVER_LOGS
685 },
686 )
687 .into(),
688 )
689 })
690 .unwrap_or_else(|| "No server selected".into());
691 let style = theme.lsp_log_menu.header.style_for(state, false);
692 Label::new(label, style.text.clone())
693 .contained()
694 .with_style(style.container)
695 })
696 .with_cursor_style(CursorStyle::PointingHand)
697 .on_click(MouseButton::Left, move |_, view, cx| {
698 view.toggle_menu(cx);
699 })
700 }
701
702 fn render_language_server_menu_item(
703 id: LanguageServerId,
704 name: LanguageServerName,
705 worktree: ModelHandle<Worktree>,
706 rpc_trace_enabled: bool,
707 logs_selected: bool,
708 rpc_trace_selected: bool,
709 theme: &Arc<Theme>,
710 cx: &mut ViewContext<Self>,
711 ) -> impl Element<Self> {
712 enum ActivateLog {}
713 enum ActivateRpcTrace {}
714
715 Flex::column()
716 .with_child({
717 let style = &theme.lsp_log_menu.server;
718 Label::new(
719 format!("{} ({})", name.0, worktree.read(cx).root_name()),
720 style.text.clone(),
721 )
722 .contained()
723 .with_style(style.container)
724 .constrained()
725 .with_height(theme.lsp_log_menu.row_height)
726 })
727 .with_child(
728 MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
729 let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
730 Label::new(SERVER_LOGS, style.text.clone())
731 .contained()
732 .with_style(style.container)
733 .constrained()
734 .with_height(theme.lsp_log_menu.row_height)
735 })
736 .with_cursor_style(CursorStyle::PointingHand)
737 .on_click(MouseButton::Left, move |_, view, cx| {
738 view.show_logs_for_server(id, cx);
739 }),
740 )
741 .with_child(
742 MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
743 let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected);
744 Flex::row()
745 .with_child(
746 Label::new(RPC_MESSAGES, style.text.clone())
747 .constrained()
748 .with_height(theme.lsp_log_menu.row_height),
749 )
750 .with_child(
751 ui::checkbox_with_label::<Self, _, Self, _>(
752 Empty::new(),
753 &theme.welcome.checkbox,
754 rpc_trace_enabled,
755 id.0,
756 cx,
757 move |this, enabled, cx| {
758 this.toggle_logging_for_server(id, enabled, cx);
759 },
760 )
761 .flex_float(),
762 )
763 .align_children_center()
764 .contained()
765 .with_style(style.container)
766 .constrained()
767 .with_height(theme.lsp_log_menu.row_height)
768 })
769 .with_cursor_style(CursorStyle::PointingHand)
770 .on_click(MouseButton::Left, move |_, view, cx| {
771 view.show_rpc_trace_for_server(id, cx);
772 }),
773 )
774 }
775}
776
777impl Entity for LogStore {
778 type Event = ();
779}
780
781impl Entity for LspLogView {
782 type Event = editor::Event;
783}
784
785impl Entity for LspLogToolbarItemView {
786 type Event = ();
787}