1use collections::{HashMap, VecDeque};
2use copilot::Copilot;
3use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
4use futures::{StreamExt, channel::mpsc};
5use gpui::{
6 AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
7 ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
8};
9use itertools::Itertools;
10use language::{LanguageServerId, language_settings::SoftWrap};
11use lsp::{
12 IoKind, LanguageServer, LanguageServerName, MessageType, SetTraceParams, TraceValue,
13 notification::SetTrace,
14};
15use project::{Project, WorktreeId, search::SearchQuery};
16use std::{any::TypeId, borrow::Cow, sync::Arc};
17use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
18use workspace::{
19 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
20 item::{Item, ItemHandle},
21 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
22};
23
24const SEND_LINE: &str = "\n// Send:";
25const RECEIVE_LINE: &str = "\n// Receive:";
26const MAX_STORED_LOG_ENTRIES: usize = 2000;
27
28pub struct LogStore {
29 projects: HashMap<WeakEntity<Project>, ProjectState>,
30 language_servers: HashMap<LanguageServerId, LanguageServerState>,
31 copilot_log_subscription: Option<lsp::Subscription>,
32 _copilot_subscription: Option<gpui::Subscription>,
33 io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
34}
35
36struct ProjectState {
37 _subscriptions: [gpui::Subscription; 2],
38}
39
40trait Message: AsRef<str> {
41 type Level: Copy + std::fmt::Debug;
42 fn should_include(&self, _: Self::Level) -> bool {
43 true
44 }
45}
46
47struct LogMessage {
48 message: String,
49 typ: MessageType,
50}
51
52impl AsRef<str> for LogMessage {
53 fn as_ref(&self) -> &str {
54 &self.message
55 }
56}
57
58impl Message for LogMessage {
59 type Level = MessageType;
60
61 fn should_include(&self, level: Self::Level) -> bool {
62 match (self.typ, level) {
63 (MessageType::ERROR, _) => true,
64 (_, MessageType::ERROR) => false,
65 (MessageType::WARNING, _) => true,
66 (_, MessageType::WARNING) => false,
67 (MessageType::INFO, _) => true,
68 (_, MessageType::INFO) => false,
69 _ => true,
70 }
71 }
72}
73
74struct TraceMessage {
75 message: String,
76}
77
78impl AsRef<str> for TraceMessage {
79 fn as_ref(&self) -> &str {
80 &self.message
81 }
82}
83
84impl Message for TraceMessage {
85 type Level = ();
86}
87
88struct RpcMessage {
89 message: String,
90}
91
92impl AsRef<str> for RpcMessage {
93 fn as_ref(&self) -> &str {
94 &self.message
95 }
96}
97
98impl Message for RpcMessage {
99 type Level = ();
100}
101
102struct LanguageServerState {
103 name: Option<LanguageServerName>,
104 worktree_id: Option<WorktreeId>,
105 kind: LanguageServerKind,
106 log_messages: VecDeque<LogMessage>,
107 trace_messages: VecDeque<TraceMessage>,
108 rpc_state: Option<LanguageServerRpcState>,
109 trace_level: TraceValue,
110 log_level: MessageType,
111 io_logs_subscription: Option<lsp::Subscription>,
112}
113
114#[derive(PartialEq, Clone)]
115pub enum LanguageServerKind {
116 Local { project: WeakEntity<Project> },
117 Remote { project: WeakEntity<Project> },
118 Global,
119}
120
121impl LanguageServerKind {
122 fn is_remote(&self) -> bool {
123 matches!(self, LanguageServerKind::Remote { .. })
124 }
125}
126
127impl std::fmt::Debug for LanguageServerKind {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
131 LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
132 LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
133 }
134 }
135}
136
137impl LanguageServerKind {
138 fn project(&self) -> Option<&WeakEntity<Project>> {
139 match self {
140 Self::Local { project } => Some(project),
141 Self::Remote { project } => Some(project),
142 Self::Global { .. } => None,
143 }
144 }
145}
146
147struct LanguageServerRpcState {
148 rpc_messages: VecDeque<RpcMessage>,
149 last_message_kind: Option<MessageKind>,
150}
151
152pub struct LspLogView {
153 pub(crate) editor: Entity<Editor>,
154 editor_subscriptions: Vec<Subscription>,
155 log_store: Entity<LogStore>,
156 current_server_id: Option<LanguageServerId>,
157 active_entry_kind: LogKind,
158 project: Entity<Project>,
159 focus_handle: FocusHandle,
160 _log_store_subscriptions: Vec<Subscription>,
161}
162
163pub struct LspLogToolbarItemView {
164 log_view: Option<Entity<LspLogView>>,
165 _log_view_subscription: Option<Subscription>,
166}
167
168#[derive(Copy, Clone, PartialEq, Eq)]
169enum MessageKind {
170 Send,
171 Receive,
172}
173
174#[derive(Clone, Copy, Debug, Default, PartialEq)]
175pub enum LogKind {
176 Rpc,
177 Trace,
178 #[default]
179 Logs,
180 ServerInfo,
181}
182
183impl LogKind {
184 fn label(&self) -> &'static str {
185 match self {
186 LogKind::Rpc => RPC_MESSAGES,
187 LogKind::Trace => SERVER_TRACE,
188 LogKind::Logs => SERVER_LOGS,
189 LogKind::ServerInfo => SERVER_INFO,
190 }
191 }
192}
193
194#[derive(Clone, Debug, PartialEq)]
195pub(crate) struct LogMenuItem {
196 pub server_id: LanguageServerId,
197 pub server_name: LanguageServerName,
198 pub worktree_root_name: String,
199 pub rpc_trace_enabled: bool,
200 pub selected_entry: LogKind,
201 pub trace_level: lsp::TraceValue,
202 pub server_kind: LanguageServerKind,
203}
204
205actions!(dev, [OpenLanguageServerLogs]);
206
207pub fn init(cx: &mut App) {
208 let log_store = cx.new(LogStore::new);
209
210 cx.observe_new(move |workspace: &mut Workspace, _, cx| {
211 let project = workspace.project();
212 if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
213 log_store.update(cx, |store, cx| {
214 store.add_project(project, cx);
215 });
216 }
217
218 let log_store = log_store.clone();
219 workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
220 let project = workspace.project().read(cx);
221 if project.is_local() || project.is_via_ssh() {
222 workspace.split_item(
223 SplitDirection::Right,
224 Box::new(cx.new(|cx| {
225 LspLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
226 })),
227 window,
228 cx,
229 );
230 }
231 });
232 })
233 .detach();
234}
235
236impl LogStore {
237 pub fn new(cx: &mut Context<Self>) -> Self {
238 let (io_tx, mut io_rx) = mpsc::unbounded();
239
240 let copilot_subscription = Copilot::global(cx).map(|copilot| {
241 let copilot = &copilot;
242 cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| {
243 if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event {
244 if let Some(server) = copilot.read(cx).language_server() {
245 let server_id = server.server_id();
246 let weak_this = cx.weak_entity();
247 this.copilot_log_subscription =
248 Some(server.on_notification::<copilot::request::LogMessage, _>(
249 move |params, cx| {
250 weak_this
251 .update(cx, |this, cx| {
252 this.add_language_server_log(
253 server_id,
254 MessageType::LOG,
255 ¶ms.message,
256 cx,
257 );
258 })
259 .ok();
260 },
261 ));
262 let name = LanguageServerName::new_static("copilot");
263 this.add_language_server(
264 LanguageServerKind::Global,
265 server.server_id(),
266 Some(name),
267 None,
268 Some(server.clone()),
269 cx,
270 );
271 }
272 }
273 })
274 });
275
276 let this = Self {
277 copilot_log_subscription: None,
278 _copilot_subscription: copilot_subscription,
279 projects: HashMap::default(),
280 language_servers: HashMap::default(),
281 io_tx,
282 };
283
284 cx.spawn(async move |this, cx| {
285 while let Some((server_id, io_kind, message)) = io_rx.next().await {
286 if let Some(this) = this.upgrade() {
287 this.update(cx, |this, cx| {
288 this.on_io(server_id, io_kind, &message, cx);
289 })?;
290 }
291 }
292 anyhow::Ok(())
293 })
294 .detach_and_log_err(cx);
295 this
296 }
297
298 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
299 let weak_project = project.downgrade();
300 self.projects.insert(
301 project.downgrade(),
302 ProjectState {
303 _subscriptions: [
304 cx.observe_release(project, move |this, _, _| {
305 this.projects.remove(&weak_project);
306 this.language_servers
307 .retain(|_, state| state.kind.project() != Some(&weak_project));
308 }),
309 cx.subscribe(project, |this, project, event, cx| {
310 let server_kind = if project.read(cx).is_via_ssh() {
311 LanguageServerKind::Remote {
312 project: project.downgrade(),
313 }
314 } else {
315 LanguageServerKind::Local {
316 project: project.downgrade(),
317 }
318 };
319
320 match event {
321 project::Event::LanguageServerAdded(id, name, worktree_id) => {
322 this.add_language_server(
323 server_kind,
324 *id,
325 Some(name.clone()),
326 *worktree_id,
327 project
328 .read(cx)
329 .lsp_store()
330 .read(cx)
331 .language_server_for_id(*id),
332 cx,
333 );
334 }
335 project::Event::LanguageServerRemoved(id) => {
336 this.remove_language_server(*id, cx);
337 }
338 project::Event::LanguageServerLog(id, typ, message) => {
339 this.add_language_server(server_kind, *id, None, None, None, cx);
340 match typ {
341 project::LanguageServerLogType::Log(typ) => {
342 this.add_language_server_log(*id, *typ, message, cx);
343 }
344 project::LanguageServerLogType::Trace(_) => {
345 this.add_language_server_trace(*id, message, cx);
346 }
347 }
348 }
349 _ => {}
350 }
351 }),
352 ],
353 },
354 );
355 }
356
357 fn get_language_server_state(
358 &mut self,
359 id: LanguageServerId,
360 ) -> Option<&mut LanguageServerState> {
361 self.language_servers.get_mut(&id)
362 }
363
364 fn add_language_server(
365 &mut self,
366 kind: LanguageServerKind,
367 server_id: LanguageServerId,
368 name: Option<LanguageServerName>,
369 worktree_id: Option<WorktreeId>,
370 server: Option<Arc<LanguageServer>>,
371 cx: &mut Context<Self>,
372 ) -> Option<&mut LanguageServerState> {
373 let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
374 cx.notify();
375 LanguageServerState {
376 name: None,
377 worktree_id: None,
378 kind,
379 rpc_state: None,
380 log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
381 trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
382 trace_level: TraceValue::Off,
383 log_level: MessageType::LOG,
384 io_logs_subscription: None,
385 }
386 });
387
388 if let Some(name) = name {
389 server_state.name = Some(name);
390 }
391 if let Some(worktree_id) = worktree_id {
392 server_state.worktree_id = Some(worktree_id);
393 }
394
395 if let Some(server) = server
396 .clone()
397 .filter(|_| server_state.io_logs_subscription.is_none())
398 {
399 let io_tx = self.io_tx.clone();
400 let server_id = server.server_id();
401 server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
402 io_tx
403 .unbounded_send((server_id, io_kind, message.to_string()))
404 .ok();
405 }));
406 }
407
408 Some(server_state)
409 }
410
411 fn add_language_server_log(
412 &mut self,
413 id: LanguageServerId,
414 typ: MessageType,
415 message: &str,
416 cx: &mut Context<Self>,
417 ) -> Option<()> {
418 let language_server_state = self.get_language_server_state(id)?;
419
420 let log_lines = &mut language_server_state.log_messages;
421 Self::add_language_server_message(
422 log_lines,
423 id,
424 LogMessage {
425 message: message.trim().to_string(),
426 typ,
427 },
428 language_server_state.log_level,
429 LogKind::Logs,
430 cx,
431 );
432 Some(())
433 }
434
435 fn add_language_server_trace(
436 &mut self,
437 id: LanguageServerId,
438 message: &str,
439 cx: &mut Context<Self>,
440 ) -> Option<()> {
441 let language_server_state = self.get_language_server_state(id)?;
442
443 let log_lines = &mut language_server_state.trace_messages;
444 Self::add_language_server_message(
445 log_lines,
446 id,
447 TraceMessage {
448 message: message.trim().to_string(),
449 },
450 (),
451 LogKind::Trace,
452 cx,
453 );
454 Some(())
455 }
456
457 fn add_language_server_message<T: Message>(
458 log_lines: &mut VecDeque<T>,
459 id: LanguageServerId,
460 message: T,
461 current_severity: <T as Message>::Level,
462 kind: LogKind,
463 cx: &mut Context<Self>,
464 ) {
465 while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
466 log_lines.pop_front();
467 }
468 let text = message.as_ref().to_string();
469 let visible = message.should_include(current_severity);
470 log_lines.push_back(message);
471
472 if visible {
473 cx.emit(Event::NewServerLogEntry { id, kind, text });
474 cx.notify();
475 }
476 }
477
478 fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
479 self.language_servers.remove(&id);
480 cx.notify();
481 }
482
483 fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
484 Some(&self.language_servers.get(&server_id)?.log_messages)
485 }
486
487 fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
488 Some(&self.language_servers.get(&server_id)?.trace_messages)
489 }
490
491 fn server_ids_for_project<'a>(
492 &'a self,
493 lookup_project: &'a WeakEntity<Project>,
494 ) -> impl Iterator<Item = LanguageServerId> + 'a {
495 self.language_servers
496 .iter()
497 .filter_map(move |(id, state)| match &state.kind {
498 LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
499 if project == lookup_project {
500 Some(*id)
501 } else {
502 None
503 }
504 }
505 LanguageServerKind::Global => Some(*id),
506 })
507 }
508
509 fn enable_rpc_trace_for_language_server(
510 &mut self,
511 server_id: LanguageServerId,
512 ) -> Option<&mut LanguageServerRpcState> {
513 let rpc_state = self
514 .language_servers
515 .get_mut(&server_id)?
516 .rpc_state
517 .get_or_insert_with(|| LanguageServerRpcState {
518 rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
519 last_message_kind: None,
520 });
521 Some(rpc_state)
522 }
523
524 pub fn disable_rpc_trace_for_language_server(
525 &mut self,
526 server_id: LanguageServerId,
527 ) -> Option<()> {
528 self.language_servers.get_mut(&server_id)?.rpc_state.take();
529 Some(())
530 }
531
532 fn on_io(
533 &mut self,
534 language_server_id: LanguageServerId,
535 io_kind: IoKind,
536 message: &str,
537 cx: &mut Context<Self>,
538 ) -> Option<()> {
539 let is_received = match io_kind {
540 IoKind::StdOut => true,
541 IoKind::StdIn => false,
542 IoKind::StdErr => {
543 self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
544 return Some(());
545 }
546 };
547
548 let state = self
549 .get_language_server_state(language_server_id)?
550 .rpc_state
551 .as_mut()?;
552 let kind = if is_received {
553 MessageKind::Receive
554 } else {
555 MessageKind::Send
556 };
557
558 let rpc_log_lines = &mut state.rpc_messages;
559 if state.last_message_kind != Some(kind) {
560 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
561 rpc_log_lines.pop_front();
562 }
563 let line_before_message = match kind {
564 MessageKind::Send => SEND_LINE,
565 MessageKind::Receive => RECEIVE_LINE,
566 };
567 rpc_log_lines.push_back(RpcMessage {
568 message: line_before_message.to_string(),
569 });
570 cx.emit(Event::NewServerLogEntry {
571 id: language_server_id,
572 kind: LogKind::Rpc,
573 text: line_before_message.to_string(),
574 });
575 }
576
577 while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
578 rpc_log_lines.pop_front();
579 }
580
581 let message = message.trim();
582 rpc_log_lines.push_back(RpcMessage {
583 message: message.to_string(),
584 });
585 cx.emit(Event::NewServerLogEntry {
586 id: language_server_id,
587 kind: LogKind::Rpc,
588 text: message.to_string(),
589 });
590 cx.notify();
591 Some(())
592 }
593}
594
595impl LspLogView {
596 pub fn new(
597 project: Entity<Project>,
598 log_store: Entity<LogStore>,
599 window: &mut Window,
600 cx: &mut Context<Self>,
601 ) -> Self {
602 let server_id = log_store
603 .read(cx)
604 .language_servers
605 .iter()
606 .find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
607 .map(|(id, _)| *id);
608
609 let weak_project = project.downgrade();
610 let model_changes_subscription =
611 cx.observe_in(&log_store, window, move |this, store, window, cx| {
612 let first_server_id_for_project =
613 store.read(cx).server_ids_for_project(&weak_project).next();
614 if let Some(current_lsp) = this.current_server_id {
615 if !store.read(cx).language_servers.contains_key(¤t_lsp) {
616 if let Some(server_id) = first_server_id_for_project {
617 match this.active_entry_kind {
618 LogKind::Rpc => {
619 this.show_rpc_trace_for_server(server_id, window, cx)
620 }
621 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
622 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
623 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
624 }
625 }
626 }
627 } else if let Some(server_id) = first_server_id_for_project {
628 match this.active_entry_kind {
629 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
630 LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
631 LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
632 LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
633 }
634 }
635
636 cx.notify();
637 });
638 let events_subscriptions = cx.subscribe_in(
639 &log_store,
640 window,
641 move |log_view, _, e, window, cx| match e {
642 Event::NewServerLogEntry { id, kind, text } => {
643 if log_view.current_server_id == Some(*id)
644 && *kind == log_view.active_entry_kind
645 {
646 log_view.editor.update(cx, |editor, cx| {
647 editor.set_read_only(false);
648 let last_offset = editor.buffer().read(cx).len(cx);
649 let newest_cursor_is_at_end =
650 editor.selections.newest::<usize>(cx).start >= last_offset;
651 editor.edit(
652 vec![
653 (last_offset..last_offset, text.as_str()),
654 (last_offset..last_offset, "\n"),
655 ],
656 cx,
657 );
658 if text.len() > 1024 {
659 if let Some((fold_offset, _)) =
660 text.char_indices().dropping(1024).next()
661 {
662 if fold_offset < text.len() {
663 editor.fold_ranges(
664 vec![
665 last_offset + fold_offset..last_offset + text.len(),
666 ],
667 false,
668 window,
669 cx,
670 );
671 }
672 }
673 }
674
675 if newest_cursor_is_at_end {
676 editor.request_autoscroll(Autoscroll::bottom(), cx);
677 }
678 editor.set_read_only(true);
679 });
680 }
681 }
682 },
683 );
684 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
685
686 let focus_handle = cx.focus_handle();
687 let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
688 window.focus(&log_view.editor.focus_handle(cx));
689 });
690
691 let mut this = Self {
692 focus_handle,
693 editor,
694 editor_subscriptions,
695 project,
696 log_store,
697 current_server_id: None,
698 active_entry_kind: LogKind::Logs,
699 _log_store_subscriptions: vec![
700 model_changes_subscription,
701 events_subscriptions,
702 focus_subscription,
703 ],
704 };
705 if let Some(server_id) = server_id {
706 this.show_logs_for_server(server_id, window, cx);
707 }
708 this
709 }
710
711 fn editor_for_logs(
712 log_contents: String,
713 window: &mut Window,
714 cx: &mut Context<Self>,
715 ) -> (Entity<Editor>, Vec<Subscription>) {
716 let editor = initialize_new_editor(log_contents, true, window, cx);
717 let editor_subscription = cx.subscribe(
718 &editor,
719 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
720 );
721 let search_subscription = cx.subscribe(
722 &editor,
723 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
724 );
725 (editor, vec![editor_subscription, search_subscription])
726 }
727
728 fn editor_for_server_info(
729 server: &LanguageServer,
730 window: &mut Window,
731 cx: &mut Context<Self>,
732 ) -> (Entity<Editor>, Vec<Subscription>) {
733 let server_info = format!(
734 "* Server: {NAME} (id {ID})
735
736* Binary: {BINARY:#?}
737
738* Registered workspace folders:
739{WORKSPACE_FOLDERS}
740
741* Capabilities: {CAPABILITIES}
742
743* Configuration: {CONFIGURATION}",
744 NAME = server.name(),
745 ID = server.server_id(),
746 BINARY = server.binary(),
747 WORKSPACE_FOLDERS = server
748 .workspace_folders()
749 .iter()
750 .filter_map(|path| path
751 .to_file_path()
752 .ok()
753 .map(|path| path.to_string_lossy().into_owned()))
754 .collect::<Vec<_>>()
755 .join(", "),
756 CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
757 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
758 CONFIGURATION = serde_json::to_string_pretty(server.configuration())
759 .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
760 );
761 let editor = initialize_new_editor(server_info, false, window, cx);
762 let editor_subscription = cx.subscribe(
763 &editor,
764 |_, _, event: &EditorEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
765 );
766 let search_subscription = cx.subscribe(
767 &editor,
768 |_, _, event: &SearchEvent, cx: &mut Context<LspLogView>| cx.emit(event.clone()),
769 );
770 (editor, vec![editor_subscription, search_subscription])
771 }
772
773 pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
774 let log_store = self.log_store.read(cx);
775
776 let unknown_server = LanguageServerName::new_static("unknown server");
777
778 let mut rows = log_store
779 .language_servers
780 .iter()
781 .map(|(server_id, state)| match &state.kind {
782 LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
783 let worktree_root_name = state
784 .worktree_id
785 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
786 .map(|worktree| worktree.read(cx).root_name().to_string())
787 .unwrap_or_else(|| "Unknown worktree".to_string());
788
789 LogMenuItem {
790 server_id: *server_id,
791 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
792 server_kind: state.kind.clone(),
793 worktree_root_name,
794 rpc_trace_enabled: state.rpc_state.is_some(),
795 selected_entry: self.active_entry_kind,
796 trace_level: lsp::TraceValue::Off,
797 }
798 }
799
800 LanguageServerKind::Global => LogMenuItem {
801 server_id: *server_id,
802 server_name: state.name.clone().unwrap_or(unknown_server.clone()),
803 server_kind: state.kind.clone(),
804 worktree_root_name: "supplementary".to_string(),
805 rpc_trace_enabled: state.rpc_state.is_some(),
806 selected_entry: self.active_entry_kind,
807 trace_level: lsp::TraceValue::Off,
808 },
809 })
810 .chain(
811 self.project
812 .read(cx)
813 .supplementary_language_servers(cx)
814 .filter_map(|(server_id, name)| {
815 let state = log_store.language_servers.get(&server_id)?;
816 Some(LogMenuItem {
817 server_id,
818 server_name: name.clone(),
819 server_kind: state.kind.clone(),
820 worktree_root_name: "supplementary".to_string(),
821 rpc_trace_enabled: state.rpc_state.is_some(),
822 selected_entry: self.active_entry_kind,
823 trace_level: lsp::TraceValue::Off,
824 })
825 }),
826 )
827 .collect::<Vec<_>>();
828 rows.sort_by_key(|row| row.server_id);
829 rows.dedup_by_key(|row| row.server_id);
830 Some(rows)
831 }
832
833 fn show_logs_for_server(
834 &mut self,
835 server_id: LanguageServerId,
836 window: &mut Window,
837 cx: &mut Context<Self>,
838 ) {
839 let typ = self
840 .log_store
841 .read(cx)
842 .language_servers
843 .get(&server_id)
844 .map(|v| v.log_level)
845 .unwrap_or(MessageType::LOG);
846 let log_contents = self
847 .log_store
848 .read(cx)
849 .server_logs(server_id)
850 .map(|v| log_contents(v, typ));
851 if let Some(log_contents) = log_contents {
852 self.current_server_id = Some(server_id);
853 self.active_entry_kind = LogKind::Logs;
854 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
855 self.editor = editor;
856 self.editor_subscriptions = editor_subscriptions;
857 cx.notify();
858 }
859 window.focus(&self.focus_handle);
860 }
861
862 fn update_log_level(
863 &self,
864 server_id: LanguageServerId,
865 level: MessageType,
866 window: &mut Window,
867 cx: &mut Context<Self>,
868 ) {
869 let log_contents = self.log_store.update(cx, |this, _| {
870 if let Some(state) = this.get_language_server_state(server_id) {
871 state.log_level = level;
872 }
873
874 this.server_logs(server_id).map(|v| log_contents(v, level))
875 });
876
877 if let Some(log_contents) = log_contents {
878 self.editor.update(cx, |editor, cx| {
879 editor.set_text(log_contents, window, cx);
880 editor.move_to_end(&MoveToEnd, window, cx);
881 });
882 cx.notify();
883 }
884
885 window.focus(&self.focus_handle);
886 }
887
888 fn show_trace_for_server(
889 &mut self,
890 server_id: LanguageServerId,
891 window: &mut Window,
892 cx: &mut Context<Self>,
893 ) {
894 let log_contents = self
895 .log_store
896 .read(cx)
897 .server_trace(server_id)
898 .map(|v| log_contents(v, ()));
899 if let Some(log_contents) = log_contents {
900 self.current_server_id = Some(server_id);
901 self.active_entry_kind = LogKind::Trace;
902 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
903 self.editor = editor;
904 self.editor_subscriptions = editor_subscriptions;
905 cx.notify();
906 }
907 window.focus(&self.focus_handle);
908 }
909
910 fn show_rpc_trace_for_server(
911 &mut self,
912 server_id: LanguageServerId,
913 window: &mut Window,
914 cx: &mut Context<Self>,
915 ) {
916 let rpc_log = self.log_store.update(cx, |log_store, _| {
917 log_store
918 .enable_rpc_trace_for_language_server(server_id)
919 .map(|state| log_contents(&state.rpc_messages, ()))
920 });
921 if let Some(rpc_log) = rpc_log {
922 self.current_server_id = Some(server_id);
923 self.active_entry_kind = LogKind::Rpc;
924 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
925 let language = self.project.read(cx).languages().language_for_name("JSON");
926 editor
927 .read(cx)
928 .buffer()
929 .read(cx)
930 .as_singleton()
931 .expect("log buffer should be a singleton")
932 .update(cx, |_, cx| {
933 cx.spawn({
934 let buffer = cx.entity();
935 async move |_, cx| {
936 let language = language.await.ok();
937 buffer.update(cx, |buffer, cx| {
938 buffer.set_language(language, cx);
939 })
940 }
941 })
942 .detach_and_log_err(cx);
943 });
944
945 self.editor = editor;
946 self.editor_subscriptions = editor_subscriptions;
947 cx.notify();
948 }
949
950 window.focus(&self.focus_handle);
951 }
952
953 fn toggle_rpc_trace_for_server(
954 &mut self,
955 server_id: LanguageServerId,
956 enabled: bool,
957 window: &mut Window,
958 cx: &mut Context<Self>,
959 ) {
960 self.log_store.update(cx, |log_store, _| {
961 if enabled {
962 log_store.enable_rpc_trace_for_language_server(server_id);
963 } else {
964 log_store.disable_rpc_trace_for_language_server(server_id);
965 }
966 });
967 if !enabled && Some(server_id) == self.current_server_id {
968 self.show_logs_for_server(server_id, window, cx);
969 cx.notify();
970 }
971 }
972
973 fn update_trace_level(
974 &self,
975 server_id: LanguageServerId,
976 level: TraceValue,
977 cx: &mut Context<Self>,
978 ) {
979 if let Some(server) = self
980 .project
981 .read(cx)
982 .lsp_store()
983 .read(cx)
984 .language_server_for_id(server_id)
985 {
986 self.log_store.update(cx, |this, _| {
987 if let Some(state) = this.get_language_server_state(server_id) {
988 state.trace_level = level;
989 }
990 });
991
992 server
993 .notify::<SetTrace>(&SetTraceParams { value: level })
994 .ok();
995 }
996 }
997
998 fn show_server_info(
999 &mut self,
1000 server_id: LanguageServerId,
1001 window: &mut Window,
1002 cx: &mut Context<Self>,
1003 ) {
1004 let lsp_store = self.project.read(cx).lsp_store();
1005 let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
1006 return;
1007 };
1008 self.current_server_id = Some(server_id);
1009 self.active_entry_kind = LogKind::ServerInfo;
1010 let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx);
1011 self.editor = editor;
1012 self.editor_subscriptions = editor_subscriptions;
1013 cx.notify();
1014 window.focus(&self.focus_handle);
1015 }
1016}
1017
1018fn log_contents<T: Message>(lines: &VecDeque<T>, level: <T as Message>::Level) -> String {
1019 lines
1020 .iter()
1021 .filter(|message| message.should_include(level))
1022 .flat_map(|message| [message.as_ref(), "\n"])
1023 .collect()
1024}
1025
1026impl Render for LspLogView {
1027 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1028 self.editor.update(cx, |editor, cx| {
1029 editor.render(window, cx).into_any_element()
1030 })
1031 }
1032}
1033
1034impl Focusable for LspLogView {
1035 fn focus_handle(&self, _: &App) -> FocusHandle {
1036 self.focus_handle.clone()
1037 }
1038}
1039
1040impl Item for LspLogView {
1041 type Event = EditorEvent;
1042
1043 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1044 Editor::to_item_events(event, f)
1045 }
1046
1047 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1048 "LSP Logs".into()
1049 }
1050
1051 fn telemetry_event_text(&self) -> Option<&'static str> {
1052 None
1053 }
1054
1055 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1056 Some(Box::new(handle.clone()))
1057 }
1058
1059 fn act_as_type<'a>(
1060 &'a self,
1061 type_id: TypeId,
1062 self_handle: &'a Entity<Self>,
1063 _: &'a App,
1064 ) -> Option<AnyView> {
1065 if type_id == TypeId::of::<Self>() {
1066 Some(self_handle.to_any())
1067 } else if type_id == TypeId::of::<Editor>() {
1068 Some(self.editor.to_any())
1069 } else {
1070 None
1071 }
1072 }
1073
1074 fn clone_on_split(
1075 &self,
1076 _workspace_id: Option<WorkspaceId>,
1077 window: &mut Window,
1078 cx: &mut Context<Self>,
1079 ) -> Option<Entity<Self>>
1080 where
1081 Self: Sized,
1082 {
1083 Some(cx.new(|cx| {
1084 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
1085 if let Some(server_id) = self.current_server_id {
1086 match self.active_entry_kind {
1087 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, window, cx),
1088 LogKind::Trace => new_view.show_trace_for_server(server_id, window, cx),
1089 LogKind::Logs => new_view.show_logs_for_server(server_id, window, cx),
1090 LogKind::ServerInfo => new_view.show_server_info(server_id, window, cx),
1091 }
1092 }
1093 new_view
1094 }))
1095 }
1096}
1097
1098impl SearchableItem for LspLogView {
1099 type Match = <Editor as SearchableItem>::Match;
1100
1101 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1102 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1103 }
1104
1105 fn update_matches(
1106 &mut self,
1107 matches: &[Self::Match],
1108 window: &mut Window,
1109 cx: &mut Context<Self>,
1110 ) {
1111 self.editor
1112 .update(cx, |e, cx| e.update_matches(matches, window, cx))
1113 }
1114
1115 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1116 self.editor
1117 .update(cx, |e, cx| e.query_suggestion(window, cx))
1118 }
1119
1120 fn activate_match(
1121 &mut self,
1122 index: usize,
1123 matches: &[Self::Match],
1124 window: &mut Window,
1125 cx: &mut Context<Self>,
1126 ) {
1127 self.editor
1128 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1129 }
1130
1131 fn select_matches(
1132 &mut self,
1133 matches: &[Self::Match],
1134 window: &mut Window,
1135 cx: &mut Context<Self>,
1136 ) {
1137 self.editor
1138 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1139 }
1140
1141 fn find_matches(
1142 &mut self,
1143 query: Arc<project::search::SearchQuery>,
1144 window: &mut Window,
1145 cx: &mut Context<Self>,
1146 ) -> gpui::Task<Vec<Self::Match>> {
1147 self.editor
1148 .update(cx, |e, cx| e.find_matches(query, window, cx))
1149 }
1150
1151 fn replace(
1152 &mut self,
1153 _: &Self::Match,
1154 _: &SearchQuery,
1155 _window: &mut Window,
1156 _: &mut Context<Self>,
1157 ) {
1158 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1159 }
1160 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1161 workspace::searchable::SearchOptions {
1162 case: true,
1163 word: true,
1164 regex: true,
1165 find_in_results: false,
1166 // LSP log is read-only.
1167 replacement: false,
1168 selection: false,
1169 }
1170 }
1171 fn active_match_index(
1172 &mut self,
1173 direction: Direction,
1174 matches: &[Self::Match],
1175 window: &mut Window,
1176 cx: &mut Context<Self>,
1177 ) -> Option<usize> {
1178 self.editor.update(cx, |e, cx| {
1179 e.active_match_index(direction, matches, window, cx)
1180 })
1181 }
1182}
1183
1184impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1185
1186impl ToolbarItemView for LspLogToolbarItemView {
1187 fn set_active_pane_item(
1188 &mut self,
1189 active_pane_item: Option<&dyn ItemHandle>,
1190 _: &mut Window,
1191 cx: &mut Context<Self>,
1192 ) -> workspace::ToolbarItemLocation {
1193 if let Some(item) = active_pane_item {
1194 if let Some(log_view) = item.downcast::<LspLogView>() {
1195 self.log_view = Some(log_view.clone());
1196 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1197 cx.notify();
1198 }));
1199 return ToolbarItemLocation::PrimaryLeft;
1200 }
1201 }
1202 self.log_view = None;
1203 self._log_view_subscription = None;
1204 ToolbarItemLocation::Hidden
1205 }
1206}
1207
1208impl Render for LspLogToolbarItemView {
1209 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1210 let Some(log_view) = self.log_view.clone() else {
1211 return div();
1212 };
1213 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1214 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1215 let current_server_id = log_view.current_server_id;
1216 (menu_rows, current_server_id)
1217 });
1218
1219 let current_server = current_server_id.and_then(|current_server_id| {
1220 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1221 Some(menu_rows[ix].clone())
1222 } else {
1223 None
1224 }
1225 });
1226 let available_language_servers: Vec<_> = menu_rows
1227 .into_iter()
1228 .map(|row| {
1229 (
1230 row.server_id,
1231 row.server_name,
1232 row.worktree_root_name,
1233 row.selected_entry,
1234 )
1235 })
1236 .collect();
1237 let log_toolbar_view = cx.entity().clone();
1238 let lsp_menu = PopoverMenu::new("LspLogView")
1239 .anchor(Corner::TopLeft)
1240 .trigger(Button::new(
1241 "language_server_menu_header",
1242 current_server
1243 .as_ref()
1244 .map(|row| {
1245 Cow::Owned(format!(
1246 "{} ({})",
1247 row.server_name.0, row.worktree_root_name,
1248 ))
1249 })
1250 .unwrap_or_else(|| "No server selected".into()),
1251 ))
1252 .menu({
1253 let log_view = log_view.clone();
1254 move |window, cx| {
1255 let log_view = log_view.clone();
1256 ContextMenu::build(window, cx, |mut menu, window, _| {
1257 for (server_id, name, worktree_root, active_entry_kind) in
1258 available_language_servers.iter()
1259 {
1260 let label = format!("{} ({})", name, worktree_root);
1261 let server_id = *server_id;
1262 let active_entry_kind = *active_entry_kind;
1263 menu = menu.entry(
1264 label,
1265 None,
1266 window.handler_for(&log_view, move |view, window, cx| {
1267 view.current_server_id = Some(server_id);
1268 view.active_entry_kind = active_entry_kind;
1269 match view.active_entry_kind {
1270 LogKind::Rpc => {
1271 view.toggle_rpc_trace_for_server(
1272 server_id, true, window, cx,
1273 );
1274 view.show_rpc_trace_for_server(server_id, window, cx);
1275 }
1276 LogKind::Trace => {
1277 view.show_trace_for_server(server_id, window, cx)
1278 }
1279 LogKind::Logs => {
1280 view.show_logs_for_server(server_id, window, cx)
1281 }
1282 LogKind::ServerInfo => {
1283 view.show_server_info(server_id, window, cx)
1284 }
1285 }
1286 cx.notify();
1287 }),
1288 );
1289 }
1290 menu
1291 })
1292 .into()
1293 }
1294 });
1295 let view_selector = current_server.map(|server| {
1296 let server_id = server.server_id;
1297 let is_remote = server.server_kind.is_remote();
1298 let rpc_trace_enabled = server.rpc_trace_enabled;
1299 let log_view = log_view.clone();
1300 PopoverMenu::new("LspViewSelector")
1301 .anchor(Corner::TopLeft)
1302 .trigger(Button::new(
1303 "language_server_menu_header",
1304 server.selected_entry.label(),
1305 ))
1306 .menu(move |window, cx| {
1307 let log_toolbar_view = log_toolbar_view.clone();
1308 let log_view = log_view.clone();
1309 Some(ContextMenu::build(window, cx, move |this, window, _| {
1310 this.entry(
1311 SERVER_LOGS,
1312 None,
1313 window.handler_for(&log_view, move |view, window, cx| {
1314 view.show_logs_for_server(server_id, window, cx);
1315 }),
1316 )
1317 .when(!is_remote, |this| {
1318 this.entry(
1319 SERVER_TRACE,
1320 None,
1321 window.handler_for(&log_view, move |view, window, cx| {
1322 view.show_trace_for_server(server_id, window, cx);
1323 }),
1324 )
1325 .custom_entry(
1326 {
1327 let log_toolbar_view = log_toolbar_view.clone();
1328 move |window, _| {
1329 h_flex()
1330 .w_full()
1331 .justify_between()
1332 .child(Label::new(RPC_MESSAGES))
1333 .child(
1334 div().child(
1335 Checkbox::new(
1336 "LspLogEnableRpcTrace",
1337 if rpc_trace_enabled {
1338 ToggleState::Selected
1339 } else {
1340 ToggleState::Unselected
1341 },
1342 )
1343 .on_click(window.listener_for(
1344 &log_toolbar_view,
1345 move |view, selection, window, cx| {
1346 let enabled = matches!(
1347 selection,
1348 ToggleState::Selected
1349 );
1350 view.toggle_rpc_logging_for_server(
1351 server_id, enabled, window, cx,
1352 );
1353 cx.stop_propagation();
1354 },
1355 )),
1356 ),
1357 )
1358 .into_any_element()
1359 }
1360 },
1361 window.handler_for(&log_view, move |view, window, cx| {
1362 view.show_rpc_trace_for_server(server_id, window, cx);
1363 }),
1364 )
1365 })
1366 .entry(
1367 SERVER_INFO,
1368 None,
1369 window.handler_for(&log_view, move |view, window, cx| {
1370 view.show_server_info(server_id, window, cx);
1371 }),
1372 )
1373 }))
1374 })
1375 });
1376 h_flex()
1377 .size_full()
1378 .justify_between()
1379 .child(
1380 h_flex()
1381 .child(lsp_menu)
1382 .children(view_selector)
1383 .child(
1384 log_view.update(cx, |this, _cx| match this.active_entry_kind {
1385 LogKind::Trace => {
1386 let log_view = log_view.clone();
1387 div().child(
1388 PopoverMenu::new("lsp-trace-level-menu")
1389 .anchor(Corner::TopLeft)
1390 .trigger(Button::new(
1391 "language_server_trace_level_selector",
1392 "Trace level",
1393 ))
1394 .menu({
1395 let log_view = log_view.clone();
1396
1397 move |window, cx| {
1398 let id = log_view.read(cx).current_server_id?;
1399
1400 let trace_level =
1401 log_view.update(cx, |this, cx| {
1402 this.log_store.update(cx, |this, _| {
1403 Some(
1404 this.get_language_server_state(id)?
1405 .trace_level,
1406 )
1407 })
1408 })?;
1409
1410 ContextMenu::build(
1411 window,
1412 cx,
1413 |mut menu, window, cx| {
1414 let log_view = log_view.clone();
1415
1416 for (option, label) in [
1417 (TraceValue::Off, "Off"),
1418 (TraceValue::Messages, "Messages"),
1419 (TraceValue::Verbose, "Verbose"),
1420 ] {
1421 menu = menu.entry(label, None, {
1422 let log_view = log_view.clone();
1423 move |_, cx| {
1424 log_view.update(cx, |this, cx| {
1425 if let Some(id) =
1426 this.current_server_id
1427 {
1428 this.update_trace_level(
1429 id, option, cx,
1430 );
1431 }
1432 });
1433 }
1434 });
1435 if option == trace_level {
1436 menu.select_last(window, cx);
1437 }
1438 }
1439
1440 menu
1441 },
1442 )
1443 .into()
1444 }
1445 }),
1446 )
1447 }
1448 LogKind::Logs => {
1449 let log_view = log_view.clone();
1450 div().child(
1451 PopoverMenu::new("lsp-log-level-menu")
1452 .anchor(Corner::TopLeft)
1453 .trigger(Button::new(
1454 "language_server_log_level_selector",
1455 "Log level",
1456 ))
1457 .menu({
1458 let log_view = log_view.clone();
1459
1460 move |window, cx| {
1461 let id = log_view.read(cx).current_server_id?;
1462
1463 let log_level =
1464 log_view.update(cx, |this, cx| {
1465 this.log_store.update(cx, |this, _| {
1466 Some(
1467 this.get_language_server_state(id)?
1468 .log_level,
1469 )
1470 })
1471 })?;
1472
1473 ContextMenu::build(
1474 window,
1475 cx,
1476 |mut menu, window, cx| {
1477 let log_view = log_view.clone();
1478
1479 for (option, label) in [
1480 (MessageType::LOG, "Log"),
1481 (MessageType::INFO, "Info"),
1482 (MessageType::WARNING, "Warning"),
1483 (MessageType::ERROR, "Error"),
1484 ] {
1485 menu = menu.entry(label, None, {
1486 let log_view = log_view.clone();
1487 move |window, cx| {
1488 log_view.update(cx, |this, cx| {
1489 if let Some(id) =
1490 this.current_server_id
1491 {
1492 this.update_log_level(
1493 id, option, window, cx,
1494 );
1495 }
1496 });
1497 }
1498 });
1499 if option == log_level {
1500 menu.select_last(window, cx);
1501 }
1502 }
1503
1504 menu
1505 },
1506 )
1507 .into()
1508 }
1509 }),
1510 )
1511 }
1512 _ => div(),
1513 }),
1514 ),
1515 )
1516 .child(
1517 div()
1518 .child(
1519 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1520 |this, _, window, cx| {
1521 if let Some(log_view) = this.log_view.as_ref() {
1522 log_view.update(cx, |log_view, cx| {
1523 log_view.editor.update(cx, |editor, cx| {
1524 editor.set_read_only(false);
1525 editor.clear(window, cx);
1526 editor.set_read_only(true);
1527 });
1528 })
1529 }
1530 },
1531 )),
1532 )
1533 .ml_2(),
1534 )
1535 }
1536}
1537
1538fn initialize_new_editor(
1539 content: String,
1540 move_to_end: bool,
1541 window: &mut Window,
1542 cx: &mut App,
1543) -> Entity<Editor> {
1544 cx.new(|cx| {
1545 let mut editor = Editor::multi_line(window, cx);
1546 editor.hide_minimap_by_default(window, cx);
1547 editor.set_text(content, window, cx);
1548 editor.set_show_git_diff_gutter(false, cx);
1549 editor.set_show_runnables(false, cx);
1550 editor.set_show_breakpoints(false, cx);
1551 editor.set_read_only(true);
1552 editor.set_show_edit_predictions(Some(false), window, cx);
1553 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1554 if move_to_end {
1555 editor.move_to_end(&MoveToEnd, window, cx);
1556 }
1557 editor
1558 })
1559}
1560
1561const RPC_MESSAGES: &str = "RPC Messages";
1562const SERVER_LOGS: &str = "Server Logs";
1563const SERVER_TRACE: &str = "Server Trace";
1564const SERVER_INFO: &str = "Server Info";
1565
1566impl Default for LspLogToolbarItemView {
1567 fn default() -> Self {
1568 Self::new()
1569 }
1570}
1571
1572impl LspLogToolbarItemView {
1573 pub fn new() -> Self {
1574 Self {
1575 log_view: None,
1576 _log_view_subscription: None,
1577 }
1578 }
1579
1580 fn toggle_rpc_logging_for_server(
1581 &mut self,
1582 id: LanguageServerId,
1583 enabled: bool,
1584 window: &mut Window,
1585 cx: &mut Context<Self>,
1586 ) {
1587 if let Some(log_view) = &self.log_view {
1588 log_view.update(cx, |log_view, cx| {
1589 log_view.toggle_rpc_trace_for_server(id, enabled, window, cx);
1590 if !enabled && Some(id) == log_view.current_server_id {
1591 log_view.show_logs_for_server(id, window, cx);
1592 cx.notify();
1593 } else if enabled {
1594 log_view.show_rpc_trace_for_server(id, window, cx);
1595 cx.notify();
1596 }
1597 window.focus(&log_view.focus_handle);
1598 });
1599 }
1600 cx.notify();
1601 }
1602}
1603
1604pub enum Event {
1605 NewServerLogEntry {
1606 id: LanguageServerId,
1607 kind: LogKind,
1608 text: String,
1609 },
1610}
1611
1612impl EventEmitter<Event> for LogStore {}
1613impl EventEmitter<Event> for LspLogView {}
1614impl EventEmitter<EditorEvent> for LspLogView {}
1615impl EventEmitter<SearchEvent> for LspLogView {}