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