1use collections::{HashMap, VecDeque};
2use copilot::Copilot;
3use editor::{actions::MoveToEnd, scroll::Autoscroll, Editor, EditorEvent};
4use futures::{channel::mpsc, StreamExt};
5use gpui::{
6 actions, div, AppContext, Context, Corner, EventEmitter, FocusHandle, FocusableView,
7 IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, View,
8 ViewContext, VisualContext, WeakModel, WindowContext,
9};
10use language::{language_settings::SoftWrap, LanguageServerId};
11use lsp::{
12 notification::SetTrace, IoKind, LanguageServer, LanguageServerName, MessageType,
13 SetTraceParams, TraceValue,
14};
15use project::{search::SearchQuery, Project, WorktreeId};
16use std::{borrow::Cow, sync::Arc};
17use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
18use workspace::{
19 item::{Item, ItemHandle},
20 searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
21 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
22};
23
24const SEND_LINE: &str = "// Send:";
25const RECEIVE_LINE: &str = "// Receive:";
26const MAX_STORED_LOG_ENTRIES: usize = 2000;
27
28pub struct LogStore {
29 projects: HashMap<WeakModel<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: WeakModel<Project> },
117 Remote { project: WeakModel<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<&WeakModel<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: View<Editor>,
154 editor_subscriptions: Vec<Subscription>,
155 log_store: Model<LogStore>,
156 current_server_id: Option<LanguageServerId>,
157 active_entry_kind: LogKind,
158 project: Model<Project>,
159 focus_handle: FocusHandle,
160 _log_store_subscriptions: Vec<Subscription>,
161}
162
163pub struct LspLogToolbarItemView {
164 log_view: Option<View<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!(debug, [OpenLanguageServerLogs]);
206
207pub fn init(cx: &mut AppContext) {
208 let log_store = cx.new_model(LogStore::new);
209
210 cx.observe_new_views(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, 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_view(|cx| {
225 LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
226 })),
227 cx,
228 );
229 }
230 });
231 })
232 .detach();
233}
234
235impl LogStore {
236 pub fn new(cx: &mut ModelContext<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_model();
246 this.copilot_log_subscription =
247 Some(server.on_notification::<copilot::request::LogMessage, _>(
248 move |params, mut cx| {
249 weak_this
250 .update(&mut 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(|this, mut cx| async move {
284 while let Some((server_id, io_kind, message)) = io_rx.next().await {
285 if let Some(this) = this.upgrade() {
286 this.update(&mut 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: &Model<Project>, cx: &mut ModelContext<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 ModelContext<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 ModelContext<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 ModelContext<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 ModelContext<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 ModelContext<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 WeakModel<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 ModelContext<Self>,
538 ) -> Option<()> {
539 let is_received = match io_kind {
540 IoKind::StdOut => true,
541 IoKind::StdIn => false,
542 IoKind::StdErr => {
543 let message = format!("stderr: {}", message.trim());
544 self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
545 return Some(());
546 }
547 };
548
549 let state = self
550 .get_language_server_state(language_server_id)?
551 .rpc_state
552 .as_mut()?;
553 let kind = if is_received {
554 MessageKind::Receive
555 } else {
556 MessageKind::Send
557 };
558
559 let rpc_log_lines = &mut state.rpc_messages;
560 if state.last_message_kind != Some(kind) {
561 let line_before_message = match kind {
562 MessageKind::Send => SEND_LINE,
563 MessageKind::Receive => RECEIVE_LINE,
564 };
565 rpc_log_lines.push_back(RpcMessage {
566 message: line_before_message.to_string(),
567 });
568 cx.emit(Event::NewServerLogEntry {
569 id: language_server_id,
570 entry: line_before_message.to_string(),
571 kind: LogKind::Rpc,
572 });
573 }
574
575 while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
576 rpc_log_lines.pop_front();
577 }
578 let message = message.trim();
579 rpc_log_lines.push_back(RpcMessage {
580 message: message.to_string(),
581 });
582 cx.emit(Event::NewServerLogEntry {
583 id: language_server_id,
584 entry: message.to_string(),
585 kind: LogKind::Rpc,
586 });
587 cx.notify();
588 Some(())
589 }
590}
591
592impl LspLogView {
593 pub fn new(
594 project: Model<Project>,
595 log_store: Model<LogStore>,
596 cx: &mut ViewContext<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 = cx.observe(&log_store, move |this, store, cx| {
607 let first_server_id_for_project =
608 store.read(cx).server_ids_for_project(&weak_project).next();
609 if let Some(current_lsp) = this.current_server_id {
610 if !store.read(cx).language_servers.contains_key(¤t_lsp) {
611 if let Some(server_id) = first_server_id_for_project {
612 match this.active_entry_kind {
613 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
614 LogKind::Trace => this.show_trace_for_server(server_id, cx),
615 LogKind::Logs => this.show_logs_for_server(server_id, cx),
616 LogKind::ServerInfo => this.show_server_info(server_id, cx),
617 }
618 } else {
619 this.current_server_id = None;
620 this.editor.update(cx, |editor, cx| {
621 editor.set_read_only(false);
622 editor.clear(cx);
623 editor.set_read_only(true);
624 });
625 cx.notify();
626 }
627 }
628 } else if let Some(server_id) = first_server_id_for_project {
629 match this.active_entry_kind {
630 LogKind::Rpc => this.show_rpc_trace_for_server(server_id, cx),
631 LogKind::Trace => this.show_trace_for_server(server_id, cx),
632 LogKind::Logs => this.show_logs_for_server(server_id, cx),
633 LogKind::ServerInfo => this.show_server_info(server_id, cx),
634 }
635 }
636
637 cx.notify();
638 });
639 let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
640 Event::NewServerLogEntry { id, entry, kind } => {
641 if log_view.current_server_id == Some(*id) && *kind == log_view.active_entry_kind {
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 cx,
660 );
661 }
662
663 if newest_cursor_is_at_end {
664 editor.request_autoscroll(Autoscroll::bottom(), cx);
665 }
666 editor.set_read_only(true);
667 });
668 }
669 }
670 });
671 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx);
672
673 let focus_handle = cx.focus_handle();
674 let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
675 cx.focus_view(&log_view.editor);
676 });
677
678 let mut this = Self {
679 focus_handle,
680 editor,
681 editor_subscriptions,
682 project,
683 log_store,
684 current_server_id: None,
685 active_entry_kind: LogKind::Logs,
686 _log_store_subscriptions: vec![
687 model_changes_subscription,
688 events_subscriptions,
689 focus_subscription,
690 ],
691 };
692 if let Some(server_id) = server_id {
693 this.show_logs_for_server(server_id, cx);
694 }
695 this
696 }
697
698 fn editor_for_logs(
699 log_contents: String,
700 cx: &mut ViewContext<Self>,
701 ) -> (View<Editor>, Vec<Subscription>) {
702 let editor = cx.new_view(|cx| {
703 let mut editor = Editor::multi_line(cx);
704 editor.set_text(log_contents, cx);
705 editor.move_to_end(&MoveToEnd, cx);
706 editor.set_read_only(true);
707 editor.set_show_inline_completions(Some(false), cx);
708 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
709 editor
710 });
711 let editor_subscription = cx.subscribe(
712 &editor,
713 |_, _, event: &EditorEvent, cx: &mut ViewContext<LspLogView>| cx.emit(event.clone()),
714 );
715 let search_subscription = cx.subscribe(
716 &editor,
717 |_, _, event: &SearchEvent, cx: &mut ViewContext<LspLogView>| cx.emit(event.clone()),
718 );
719 (editor, vec![editor_subscription, search_subscription])
720 }
721
722 fn editor_for_server_info(
723 server: &LanguageServer,
724 cx: &mut ViewContext<Self>,
725 ) -> (View<Editor>, Vec<Subscription>) {
726 let editor = cx.new_view(|cx| {
727 let mut editor = Editor::multi_line(cx);
728 let server_info = format!(
729 "* Server: {NAME} (id {ID})
730
731* Binary: {BINARY:#?}
732
733* Registered workspace folders:
734{WORKSPACE_FOLDERS}
735
736* Capabilities: {CAPABILITIES}
737
738* Configuration: {CONFIGURATION}",
739 NAME = server.name(),
740 ID = server.server_id(),
741 BINARY = server.binary(),
742 WORKSPACE_FOLDERS = server
743 .workspace_folders()
744 .iter()
745 .filter_map(|path| path
746 .to_file_path()
747 .ok()
748 .map(|path| path.to_string_lossy().into_owned()))
749 .collect::<Vec<_>>()
750 .join(", "),
751 CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
752 .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
753 CONFIGURATION = serde_json::to_string_pretty(server.configuration())
754 .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
755 );
756 editor.set_text(server_info, cx);
757 editor.set_read_only(true);
758 editor.set_show_inline_completions(Some(false), cx);
759 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
760 editor
761 });
762 let editor_subscription = cx.subscribe(
763 &editor,
764 |_, _, event: &EditorEvent, cx: &mut ViewContext<LspLogView>| cx.emit(event.clone()),
765 );
766 let search_subscription = cx.subscribe(
767 &editor,
768 |_, _, event: &SearchEvent, cx: &mut ViewContext<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 AppContext) -> 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(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
834 let typ = self
835 .log_store
836 .read_with(cx, |v, _| {
837 v.language_servers.get(&server_id).map(|v| v.log_level)
838 })
839 .unwrap_or(MessageType::LOG);
840 let log_contents = self
841 .log_store
842 .read(cx)
843 .server_logs(server_id)
844 .map(|v| log_contents(v, typ));
845 if let Some(log_contents) = log_contents {
846 self.current_server_id = Some(server_id);
847 self.active_entry_kind = LogKind::Logs;
848 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
849 self.editor = editor;
850 self.editor_subscriptions = editor_subscriptions;
851 cx.notify();
852 }
853 cx.focus(&self.focus_handle);
854 }
855
856 fn update_log_level(
857 &self,
858 server_id: LanguageServerId,
859 level: MessageType,
860 cx: &mut ViewContext<Self>,
861 ) {
862 let log_contents = self.log_store.update(cx, |this, _| {
863 if let Some(state) = this.get_language_server_state(server_id) {
864 state.log_level = level;
865 }
866
867 this.server_logs(server_id).map(|v| log_contents(v, level))
868 });
869
870 if let Some(log_contents) = log_contents {
871 self.editor.update(cx, move |editor, cx| {
872 editor.set_text(log_contents, cx);
873 editor.move_to_end(&MoveToEnd, cx);
874 });
875 cx.notify();
876 }
877
878 cx.focus(&self.focus_handle);
879 }
880
881 fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
882 let log_contents = self
883 .log_store
884 .read(cx)
885 .server_trace(server_id)
886 .map(|v| log_contents(v, ()));
887 if let Some(log_contents) = log_contents {
888 self.current_server_id = Some(server_id);
889 self.active_entry_kind = LogKind::Trace;
890 let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, cx);
891 self.editor = editor;
892 self.editor_subscriptions = editor_subscriptions;
893 cx.notify();
894 }
895 cx.focus(&self.focus_handle);
896 }
897
898 fn show_rpc_trace_for_server(
899 &mut self,
900 server_id: LanguageServerId,
901 cx: &mut ViewContext<Self>,
902 ) {
903 let rpc_log = self.log_store.update(cx, |log_store, _| {
904 log_store
905 .enable_rpc_trace_for_language_server(server_id)
906 .map(|state| log_contents(&state.rpc_messages, ()))
907 });
908 if let Some(rpc_log) = rpc_log {
909 self.current_server_id = Some(server_id);
910 self.active_entry_kind = LogKind::Rpc;
911 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx);
912 let language = self.project.read(cx).languages().language_for_name("JSON");
913 editor
914 .read(cx)
915 .buffer()
916 .read(cx)
917 .as_singleton()
918 .expect("log buffer should be a singleton")
919 .update(cx, |_, cx| {
920 cx.spawn({
921 let buffer = cx.handle();
922 |_, mut cx| async move {
923 let language = language.await.ok();
924 buffer.update(&mut cx, |buffer, cx| {
925 buffer.set_language(language, cx);
926 })
927 }
928 })
929 .detach_and_log_err(cx);
930 });
931
932 self.editor = editor;
933 self.editor_subscriptions = editor_subscriptions;
934 cx.notify();
935 }
936
937 cx.focus(&self.focus_handle);
938 }
939
940 fn toggle_rpc_trace_for_server(
941 &mut self,
942 server_id: LanguageServerId,
943 enabled: bool,
944 cx: &mut ViewContext<Self>,
945 ) {
946 self.log_store.update(cx, |log_store, _| {
947 if enabled {
948 log_store.enable_rpc_trace_for_language_server(server_id);
949 } else {
950 log_store.disable_rpc_trace_for_language_server(server_id);
951 }
952 });
953 if !enabled && Some(server_id) == self.current_server_id {
954 self.show_logs_for_server(server_id, cx);
955 cx.notify();
956 }
957 }
958
959 fn update_trace_level(
960 &self,
961 server_id: LanguageServerId,
962 level: TraceValue,
963 cx: &mut ViewContext<Self>,
964 ) {
965 if let Some(server) = self
966 .project
967 .read(cx)
968 .lsp_store()
969 .read(cx)
970 .language_server_for_id(server_id)
971 {
972 self.log_store.update(cx, |this, _| {
973 if let Some(state) = this.get_language_server_state(server_id) {
974 state.trace_level = level;
975 }
976 });
977
978 server
979 .notify::<SetTrace>(&SetTraceParams { value: level })
980 .ok();
981 }
982 }
983
984 fn show_server_info(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
985 let lsp_store = self.project.read(cx).lsp_store();
986 let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else {
987 return;
988 };
989 self.current_server_id = Some(server_id);
990 self.active_entry_kind = LogKind::ServerInfo;
991 let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, cx);
992 self.editor = editor;
993 self.editor_subscriptions = editor_subscriptions;
994 cx.notify();
995 cx.focus(&self.focus_handle);
996 }
997}
998
999fn log_filter<T: Message>(line: &T, cmp: <T as Message>::Level) -> Option<&str> {
1000 if line.should_include(cmp) {
1001 Some(line.as_ref())
1002 } else {
1003 None
1004 }
1005}
1006
1007fn log_contents<T: Message>(lines: &VecDeque<T>, cmp: <T as Message>::Level) -> String {
1008 let (a, b) = lines.as_slices();
1009 let a = a.iter().filter_map(move |v| log_filter(v, cmp));
1010 let b = b.iter().filter_map(move |v| log_filter(v, cmp));
1011 a.chain(b).fold(String::new(), |mut acc, el| {
1012 acc.push_str(el);
1013 acc.push('\n');
1014 acc
1015 })
1016}
1017
1018impl Render for LspLogView {
1019 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1020 self.editor
1021 .update(cx, |editor, cx| editor.render(cx).into_any_element())
1022 }
1023}
1024
1025impl FocusableView for LspLogView {
1026 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
1027 self.focus_handle.clone()
1028 }
1029}
1030
1031impl Item for LspLogView {
1032 type Event = EditorEvent;
1033
1034 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
1035 Editor::to_item_events(event, f)
1036 }
1037
1038 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
1039 Some("LSP Logs".into())
1040 }
1041
1042 fn telemetry_event_text(&self) -> Option<&'static str> {
1043 None
1044 }
1045
1046 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1047 Some(Box::new(handle.clone()))
1048 }
1049
1050 fn clone_on_split(
1051 &self,
1052 _workspace_id: Option<WorkspaceId>,
1053 cx: &mut ViewContext<Self>,
1054 ) -> Option<View<Self>>
1055 where
1056 Self: Sized,
1057 {
1058 Some(cx.new_view(|cx| {
1059 let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), cx);
1060 if let Some(server_id) = self.current_server_id {
1061 match self.active_entry_kind {
1062 LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, cx),
1063 LogKind::Trace => new_view.show_trace_for_server(server_id, cx),
1064 LogKind::Logs => new_view.show_logs_for_server(server_id, cx),
1065 LogKind::ServerInfo => new_view.show_server_info(server_id, cx),
1066 }
1067 }
1068 new_view
1069 }))
1070 }
1071}
1072
1073impl SearchableItem for LspLogView {
1074 type Match = <Editor as SearchableItem>::Match;
1075
1076 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
1077 self.editor.update(cx, |e, cx| e.clear_matches(cx))
1078 }
1079
1080 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1081 self.editor
1082 .update(cx, |e, cx| e.update_matches(matches, cx))
1083 }
1084
1085 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
1086 self.editor.update(cx, |e, cx| e.query_suggestion(cx))
1087 }
1088
1089 fn activate_match(
1090 &mut self,
1091 index: usize,
1092 matches: &[Self::Match],
1093 cx: &mut ViewContext<Self>,
1094 ) {
1095 self.editor
1096 .update(cx, |e, cx| e.activate_match(index, matches, cx))
1097 }
1098
1099 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1100 self.editor
1101 .update(cx, |e, cx| e.select_matches(matches, cx))
1102 }
1103
1104 fn find_matches(
1105 &mut self,
1106 query: Arc<project::search::SearchQuery>,
1107 cx: &mut ViewContext<Self>,
1108 ) -> gpui::Task<Vec<Self::Match>> {
1109 self.editor.update(cx, |e, cx| e.find_matches(query, cx))
1110 }
1111
1112 fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
1113 // Since LSP Log is read-only, it doesn't make sense to support replace operation.
1114 }
1115 fn supported_options() -> workspace::searchable::SearchOptions {
1116 workspace::searchable::SearchOptions {
1117 case: true,
1118 word: true,
1119 regex: true,
1120 // LSP log is read-only.
1121 replacement: false,
1122 selection: false,
1123 }
1124 }
1125 fn active_match_index(
1126 &mut self,
1127 matches: &[Self::Match],
1128 cx: &mut ViewContext<Self>,
1129 ) -> Option<usize> {
1130 self.editor
1131 .update(cx, |e, cx| e.active_match_index(matches, cx))
1132 }
1133}
1134
1135impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
1136
1137impl ToolbarItemView for LspLogToolbarItemView {
1138 fn set_active_pane_item(
1139 &mut self,
1140 active_pane_item: Option<&dyn ItemHandle>,
1141 cx: &mut ViewContext<Self>,
1142 ) -> workspace::ToolbarItemLocation {
1143 if let Some(item) = active_pane_item {
1144 if let Some(log_view) = item.downcast::<LspLogView>() {
1145 self.log_view = Some(log_view.clone());
1146 self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
1147 cx.notify();
1148 }));
1149 return ToolbarItemLocation::PrimaryLeft;
1150 }
1151 }
1152 self.log_view = None;
1153 self._log_view_subscription = None;
1154 ToolbarItemLocation::Hidden
1155 }
1156}
1157
1158impl Render for LspLogToolbarItemView {
1159 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1160 let Some(log_view) = self.log_view.clone() else {
1161 return div();
1162 };
1163 let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
1164 let menu_rows = log_view.menu_items(cx).unwrap_or_default();
1165 let current_server_id = log_view.current_server_id;
1166 (menu_rows, current_server_id)
1167 });
1168
1169 let current_server = current_server_id.and_then(|current_server_id| {
1170 if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
1171 Some(menu_rows[ix].clone())
1172 } else {
1173 None
1174 }
1175 });
1176 let available_language_servers: Vec<_> = menu_rows
1177 .iter()
1178 .map(|row| {
1179 (
1180 row.server_id,
1181 row.server_name.clone(),
1182 row.worktree_root_name.clone(),
1183 row.selected_entry,
1184 )
1185 })
1186 .collect();
1187 let log_toolbar_view = cx.view().clone();
1188 let lsp_menu = PopoverMenu::new("LspLogView")
1189 .anchor(Corner::TopLeft)
1190 .trigger(Button::new(
1191 "language_server_menu_header",
1192 current_server
1193 .as_ref()
1194 .map(|row| {
1195 Cow::Owned(format!(
1196 "{} ({})",
1197 row.server_name.0, row.worktree_root_name,
1198 ))
1199 })
1200 .unwrap_or_else(|| "No server selected".into()),
1201 ))
1202 .menu({
1203 let log_view = log_view.clone();
1204 move |cx| {
1205 let log_view = log_view.clone();
1206 ContextMenu::build(cx, |mut menu, cx| {
1207 for (server_id, name, worktree_root, active_entry_kind) in
1208 available_language_servers.iter()
1209 {
1210 let label = format!("{} ({})", name, worktree_root);
1211 let server_id = *server_id;
1212 let active_entry_kind = *active_entry_kind;
1213 menu = menu.entry(
1214 label,
1215 None,
1216 cx.handler_for(&log_view, move |view, cx| {
1217 view.current_server_id = Some(server_id);
1218 view.active_entry_kind = active_entry_kind;
1219 match view.active_entry_kind {
1220 LogKind::Rpc => {
1221 view.toggle_rpc_trace_for_server(server_id, true, cx);
1222 view.show_rpc_trace_for_server(server_id, cx);
1223 }
1224 LogKind::Trace => view.show_trace_for_server(server_id, cx),
1225 LogKind::Logs => view.show_logs_for_server(server_id, cx),
1226 LogKind::ServerInfo => view.show_server_info(server_id, cx),
1227 }
1228 cx.notify();
1229 }),
1230 );
1231 }
1232 menu
1233 })
1234 .into()
1235 }
1236 });
1237 let view_selector = current_server.map(|server| {
1238 let server_id = server.server_id;
1239 let is_remote = server.server_kind.is_remote();
1240 let rpc_trace_enabled = server.rpc_trace_enabled;
1241 let log_view = log_view.clone();
1242 PopoverMenu::new("LspViewSelector")
1243 .anchor(Corner::TopLeft)
1244 .trigger(Button::new(
1245 "language_server_menu_header",
1246 server.selected_entry.label(),
1247 ))
1248 .menu(move |cx| {
1249 let log_toolbar_view = log_toolbar_view.clone();
1250 let log_view = log_view.clone();
1251 Some(ContextMenu::build(cx, move |this, cx| {
1252 this.entry(
1253 SERVER_LOGS,
1254 None,
1255 cx.handler_for(&log_view, move |view, cx| {
1256 view.show_logs_for_server(server_id, cx);
1257 }),
1258 )
1259 .when(!is_remote, |this| {
1260 this.entry(
1261 SERVER_TRACE,
1262 None,
1263 cx.handler_for(&log_view, move |view, cx| {
1264 view.show_trace_for_server(server_id, cx);
1265 }),
1266 )
1267 .custom_entry(
1268 {
1269 let log_toolbar_view = log_toolbar_view.clone();
1270 move |cx| {
1271 h_flex()
1272 .w_full()
1273 .justify_between()
1274 .child(Label::new(RPC_MESSAGES))
1275 .child(
1276 div().child(
1277 Checkbox::new(
1278 "LspLogEnableRpcTrace",
1279 if rpc_trace_enabled {
1280 ToggleState::Selected
1281 } else {
1282 ToggleState::Unselected
1283 },
1284 )
1285 .on_click(cx.listener_for(
1286 &log_toolbar_view,
1287 move |view, selection, cx| {
1288 let enabled = matches!(
1289 selection,
1290 ToggleState::Selected
1291 );
1292 view.toggle_rpc_logging_for_server(
1293 server_id, enabled, cx,
1294 );
1295 cx.stop_propagation();
1296 },
1297 )),
1298 ),
1299 )
1300 .into_any_element()
1301 }
1302 },
1303 cx.handler_for(&log_view, move |view, cx| {
1304 view.show_rpc_trace_for_server(server_id, cx);
1305 }),
1306 )
1307 })
1308 .entry(
1309 SERVER_INFO,
1310 None,
1311 cx.handler_for(&log_view, move |view, cx| {
1312 view.show_server_info(server_id, cx);
1313 }),
1314 )
1315 }))
1316 })
1317 });
1318 h_flex()
1319 .size_full()
1320 .justify_between()
1321 .child(
1322 h_flex()
1323 .child(lsp_menu)
1324 .children(view_selector)
1325 .child(log_view.update(cx, |this, _| match this.active_entry_kind {
1326 LogKind::Trace => {
1327 let log_view = log_view.clone();
1328 div().child(
1329 PopoverMenu::new("lsp-trace-level-menu")
1330 .anchor(Corner::TopLeft)
1331 .trigger(Button::new(
1332 "language_server_trace_level_selector",
1333 "Trace level",
1334 ))
1335 .menu({
1336 let log_view = log_view.clone();
1337
1338 move |cx| {
1339 let id = log_view.read(cx).current_server_id?;
1340
1341 let trace_level = log_view.update(cx, |this, cx| {
1342 this.log_store.update(cx, |this, _| {
1343 Some(
1344 this.get_language_server_state(id)?
1345 .trace_level,
1346 )
1347 })
1348 })?;
1349
1350 ContextMenu::build(cx, |mut menu, _| {
1351 let log_view = log_view.clone();
1352
1353 for (option, label) in [
1354 (TraceValue::Off, "Off"),
1355 (TraceValue::Messages, "Messages"),
1356 (TraceValue::Verbose, "Verbose"),
1357 ] {
1358 menu = menu.entry(label, None, {
1359 let log_view = log_view.clone();
1360 move |cx| {
1361 log_view.update(cx, |this, cx| {
1362 if let Some(id) =
1363 this.current_server_id
1364 {
1365 this.update_trace_level(
1366 id, option, cx,
1367 );
1368 }
1369 });
1370 }
1371 });
1372 if option == trace_level {
1373 menu.select_last();
1374 }
1375 }
1376
1377 menu
1378 })
1379 .into()
1380 }
1381 }),
1382 )
1383 }
1384 LogKind::Logs => {
1385 let log_view = log_view.clone();
1386 div().child(
1387 PopoverMenu::new("lsp-log-level-menu")
1388 .anchor(Corner::TopLeft)
1389 .trigger(Button::new(
1390 "language_server_log_level_selector",
1391 "Log level",
1392 ))
1393 .menu({
1394 let log_view = log_view.clone();
1395
1396 move |cx| {
1397 let id = log_view.read(cx).current_server_id?;
1398
1399 let log_level = log_view.update(cx, |this, cx| {
1400 this.log_store.update(cx, |this, _| {
1401 Some(
1402 this.get_language_server_state(id)?
1403 .log_level,
1404 )
1405 })
1406 })?;
1407
1408 ContextMenu::build(cx, |mut menu, _| {
1409 let log_view = log_view.clone();
1410
1411 for (option, label) in [
1412 (MessageType::LOG, "Log"),
1413 (MessageType::INFO, "Info"),
1414 (MessageType::WARNING, "Warning"),
1415 (MessageType::ERROR, "Error"),
1416 ] {
1417 menu = menu.entry(label, None, {
1418 let log_view = log_view.clone();
1419 move |cx| {
1420 log_view.update(cx, |this, cx| {
1421 if let Some(id) =
1422 this.current_server_id
1423 {
1424 this.update_log_level(
1425 id, option, cx,
1426 );
1427 }
1428 });
1429 }
1430 });
1431 if option == log_level {
1432 menu.select_last();
1433 }
1434 }
1435
1436 menu
1437 })
1438 .into()
1439 }
1440 }),
1441 )
1442 }
1443 _ => div(),
1444 })),
1445 )
1446 .child(
1447 div()
1448 .child(
1449 Button::new("clear_log_button", "Clear").on_click(cx.listener(
1450 |this, _, cx| {
1451 if let Some(log_view) = this.log_view.as_ref() {
1452 log_view.update(cx, |log_view, cx| {
1453 log_view.editor.update(cx, |editor, cx| {
1454 editor.set_read_only(false);
1455 editor.clear(cx);
1456 editor.set_read_only(true);
1457 });
1458 })
1459 }
1460 },
1461 )),
1462 )
1463 .ml_2(),
1464 )
1465 }
1466}
1467
1468const RPC_MESSAGES: &str = "RPC Messages";
1469const SERVER_LOGS: &str = "Server Logs";
1470const SERVER_TRACE: &str = "Server Trace";
1471const SERVER_INFO: &str = "Server Info";
1472
1473impl Default for LspLogToolbarItemView {
1474 fn default() -> Self {
1475 Self::new()
1476 }
1477}
1478
1479impl LspLogToolbarItemView {
1480 pub fn new() -> Self {
1481 Self {
1482 log_view: None,
1483 _log_view_subscription: None,
1484 }
1485 }
1486
1487 fn toggle_rpc_logging_for_server(
1488 &mut self,
1489 id: LanguageServerId,
1490 enabled: bool,
1491 cx: &mut ViewContext<Self>,
1492 ) {
1493 if let Some(log_view) = &self.log_view {
1494 log_view.update(cx, |log_view, cx| {
1495 log_view.toggle_rpc_trace_for_server(id, enabled, cx);
1496 if !enabled && Some(id) == log_view.current_server_id {
1497 log_view.show_logs_for_server(id, cx);
1498 cx.notify();
1499 } else if enabled {
1500 log_view.show_rpc_trace_for_server(id, cx);
1501 cx.notify();
1502 }
1503 cx.focus(&log_view.focus_handle);
1504 });
1505 }
1506 cx.notify();
1507 }
1508}
1509
1510pub enum Event {
1511 NewServerLogEntry {
1512 id: LanguageServerId,
1513 entry: String,
1514 kind: LogKind,
1515 },
1516}
1517
1518impl EventEmitter<Event> for LogStore {}
1519impl EventEmitter<Event> for LspLogView {}
1520impl EventEmitter<EditorEvent> for LspLogView {}
1521impl EventEmitter<SearchEvent> for LspLogView {}