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