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