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