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