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