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