1use crate::WitBoundary;
23use crate::boundary::path_to_wit;
24use crate::lattice::plugin_host::types::{
25 Event as WitEvent, EventAppliedEdit as WitEventAppliedEdit, EventDocumentChanged,
26 EventDocumentOpened, EventDocumentPath, EventFilter as WitEventFilter,
27 EventKind as WitEventKind, EventModalModeChanged, EventModeLifecycle, EventOptionChanged,
28 EventPlugin as WitEventPlugin, EventPluginLifecycle as WitEventPluginLifecycle,
29 EventSelectionsChanged,
30};
31use lattice_keymap::ModeId;
32use lattice_protocol::event::AppliedEdit as NativeEventAppliedEdit;
33use lattice_protocol::ids::{BufferId, DocumentId};
34use lattice_protocol::position::Range as NativeRange;
35use lattice_protocol::selection::SelectionSet as NativeSelectionSet;
36use lattice_protocol::{Event as NativeEvent, EventKind as NativeEventKind};
37use lattice_runtime::{EventFilter as NativeEventFilter, compile_glob_set};
38
39fn opt_path_to_wit(path: &Option<std::path::PathBuf>) -> Result<Option<String>, String> {
43 path.as_ref().map(|p| path_to_wit(p)).transpose()
44}
45
46impl WitBoundary for NativeEventAppliedEdit {
47 type Wit = WitEventAppliedEdit;
48
49 fn to_wit(&self) -> Result<WitEventAppliedEdit, String> {
50 Ok(WitEventAppliedEdit {
51 original_range: self.original_range.to_wit()?,
52 inserted_range: self.inserted_range.to_wit()?,
53 replaced_text: self.replaced_text.clone(),
54 inserted_text: self.inserted_text.clone(),
55 })
56 }
57
58 fn from_wit(wit: WitEventAppliedEdit) -> Result<Self, String> {
59 Ok(NativeEventAppliedEdit {
60 original_range: NativeRange::from_wit(wit.original_range)?,
61 inserted_range: NativeRange::from_wit(wit.inserted_range)?,
62 replaced_text: wit.replaced_text,
63 inserted_text: wit.inserted_text,
64 })
65 }
66}
67
68impl WitBoundary for NativeEventKind {
69 type Wit = WitEventKind;
70
71 fn to_wit(&self) -> Result<WitEventKind, String> {
72 Ok(match self {
73 NativeEventKind::DocumentOpened => WitEventKind::DocumentOpened,
74 NativeEventKind::DocumentClosed => WitEventKind::DocumentClosed,
75 NativeEventKind::BeforeSave => WitEventKind::BeforeSave,
76 NativeEventKind::DocumentSaved => WitEventKind::DocumentSaved,
77 NativeEventKind::DocumentChanged => WitEventKind::DocumentChanged,
78 NativeEventKind::SelectionsChanged => WitEventKind::SelectionsChanged,
79 NativeEventKind::ModalModeChanged => WitEventKind::ModalModeChanged,
80 NativeEventKind::BeforeQuit => WitEventKind::BeforeQuit,
81 NativeEventKind::OptionChanged => WitEventKind::OptionChanged,
82 NativeEventKind::MajorEntered => WitEventKind::MajorEntered,
83 NativeEventKind::MajorExiting => WitEventKind::MajorExiting,
84 NativeEventKind::MinorActivated => WitEventKind::MinorActivated,
85 NativeEventKind::MinorDeactivated => WitEventKind::MinorDeactivated,
86 NativeEventKind::Plugin => WitEventKind::Plugin,
87 NativeEventKind::PluginCrashed => {
94 return Err(
95 "event-kind `plugin-crashed` is host-internal, not deliverable to plugins"
96 .to_string(),
97 );
98 }
99 NativeEventKind::PrePluginLoaded => WitEventKind::PrePluginLoaded,
102 NativeEventKind::PluginLoaded => WitEventKind::PluginLoaded,
105 NativeEventKind::PluginUnloaded => WitEventKind::PluginUnloaded,
106 NativeEventKind::BufferOptionOverrideRequested => {
109 return Err(
110 "event-kind `buffer-option-override-requested` is host-internal, not \
111 deliverable to plugins"
112 .to_string(),
113 );
114 }
115 NativeEventKind::ModeEnablementRequested => {
116 return Err(
117 "event-kind `mode-enablement-requested` is host-internal, not deliverable to plugins"
118 .to_string(),
119 );
120 }
121 NativeEventKind::BackgroundTaskFinished => {
128 return Err(
129 "event-kind `background-task-finished` is not yet mirrored in WIT".to_string(),
130 );
131 }
132 NativeEventKind::FilesChanged => WitEventKind::FilesChanged,
134 })
135 }
136
137 fn from_wit(wit: WitEventKind) -> Result<Self, String> {
138 Ok(match wit {
139 WitEventKind::DocumentOpened => NativeEventKind::DocumentOpened,
140 WitEventKind::DocumentClosed => NativeEventKind::DocumentClosed,
141 WitEventKind::BeforeSave => NativeEventKind::BeforeSave,
142 WitEventKind::DocumentSaved => NativeEventKind::DocumentSaved,
143 WitEventKind::DocumentChanged => NativeEventKind::DocumentChanged,
144 WitEventKind::SelectionsChanged => NativeEventKind::SelectionsChanged,
145 WitEventKind::ModalModeChanged => NativeEventKind::ModalModeChanged,
146 WitEventKind::BeforeQuit => NativeEventKind::BeforeQuit,
147 WitEventKind::OptionChanged => NativeEventKind::OptionChanged,
148 WitEventKind::MajorEntered => NativeEventKind::MajorEntered,
149 WitEventKind::MajorExiting => NativeEventKind::MajorExiting,
150 WitEventKind::MinorActivated => NativeEventKind::MinorActivated,
151 WitEventKind::MinorDeactivated => NativeEventKind::MinorDeactivated,
152 WitEventKind::Plugin => NativeEventKind::Plugin,
153 WitEventKind::PrePluginLoaded => NativeEventKind::PrePluginLoaded,
154 WitEventKind::PluginLoaded => NativeEventKind::PluginLoaded,
155 WitEventKind::PluginUnloaded => NativeEventKind::PluginUnloaded,
156 WitEventKind::FilesChanged => NativeEventKind::FilesChanged,
158 })
159 }
160}
161
162impl WitBoundary for NativeEvent {
163 type Wit = WitEvent;
164
165 fn to_wit(&self) -> Result<WitEvent, String> {
168 Ok(match self {
169 NativeEvent::DocumentOpened {
170 id,
171 path,
172 version,
173 text,
174 } => WitEvent::DocumentOpened(EventDocumentOpened {
175 id: id.raw(),
176 path: opt_path_to_wit(path)?,
177 version: *version,
178 text: text.clone(),
179 }),
180 NativeEvent::DocumentClosed { id } => WitEvent::DocumentClosed(id.raw()),
181 NativeEvent::BeforeSave { id, path } => WitEvent::BeforeSave(EventDocumentPath {
182 id: id.raw(),
183 path: path_to_wit(path)?,
184 }),
185 NativeEvent::DocumentSaved { id, path } => WitEvent::DocumentSaved(EventDocumentPath {
186 id: id.raw(),
187 path: path_to_wit(path)?,
188 }),
189 NativeEvent::DocumentChanged {
190 id,
191 path,
192 version,
193 edits,
194 } => WitEvent::DocumentChanged(EventDocumentChanged {
195 id: id.raw(),
196 path: opt_path_to_wit(path)?,
197 version: *version,
198 edits: edits
199 .iter()
200 .map(WitBoundary::to_wit)
201 .collect::<Result<Vec<_>, _>>()?,
202 }),
203 NativeEvent::SelectionsChanged {
204 id,
205 version,
206 selections,
207 } => WitEvent::SelectionsChanged(EventSelectionsChanged {
208 id: id.raw(),
209 version: *version,
210 selections: selections.to_wit()?,
211 }),
212 NativeEvent::ModalModeChanged { from, to } => {
213 WitEvent::ModalModeChanged(EventModalModeChanged {
214 from_state: from.clone(),
215 to_state: to.clone(),
216 })
217 }
218 NativeEvent::BeforeQuit => WitEvent::BeforeQuit,
219 NativeEvent::OptionChanged { name, old, new } => {
220 WitEvent::OptionChanged(EventOptionChanged {
221 name: name.clone(),
222 old: old.clone(),
223 new_value: new.clone(),
224 })
225 }
226 NativeEvent::MajorEntered { buffer, major } => {
227 WitEvent::MajorEntered(mode_lifecycle(buffer, major))
228 }
229 NativeEvent::MajorExiting { buffer, major } => {
230 WitEvent::MajorExiting(mode_lifecycle(buffer, major))
231 }
232 NativeEvent::MinorActivated { buffer, minor } => {
233 WitEvent::MinorActivated(mode_lifecycle(buffer, minor))
234 }
235 NativeEvent::MinorDeactivated { buffer, minor } => {
236 WitEvent::MinorDeactivated(mode_lifecycle(buffer, minor))
237 }
238 NativeEvent::Plugin { name, payload } => WitEvent::Plugin(WitEventPlugin {
240 name: name.clone(),
241 payload: payload.clone(),
242 }),
243 NativeEvent::PluginCrashed { .. } => {
247 return Err(
248 "event `plugin-crashed` is host-internal, not deliverable to plugins"
249 .to_string(),
250 );
251 }
252 NativeEvent::PrePluginLoaded { name } => WitEvent::PrePluginLoaded(name.clone()),
254 NativeEvent::PluginLoaded { name, id } => {
256 WitEvent::PluginLoaded(WitEventPluginLifecycle {
257 name: name.clone(),
258 id: *id,
259 })
260 }
261 NativeEvent::PluginUnloaded { name, id } => {
262 WitEvent::PluginUnloaded(WitEventPluginLifecycle {
263 name: name.clone(),
264 id: *id,
265 })
266 }
267 NativeEvent::BufferOptionOverrideRequested { .. } => {
269 return Err(
270 "event `buffer-option-override-requested` is host-internal, not \
271 deliverable to plugins"
272 .to_string(),
273 );
274 }
275 NativeEvent::ModeEnablementRequested { .. } => {
276 return Err(
277 "event `mode-enablement-requested` is host-internal, not deliverable to plugins"
278 .to_string(),
279 );
280 }
281 NativeEvent::BackgroundTaskFinished { .. } => {
283 return Err(
284 "event `background-task-finished` is not yet mirrored in WIT".to_string(),
285 );
286 }
287 NativeEvent::FilesChanged { paths, .. } => WitEvent::FilesChanged(
294 paths
295 .iter()
296 .filter_map(|p| p.to_str().map(str::to_string))
297 .collect(),
298 ),
299 })
300 }
301
302 fn from_wit(wit: WitEvent) -> Result<Self, String> {
303 Ok(match wit {
304 WitEvent::DocumentOpened(p) => NativeEvent::DocumentOpened {
305 id: DocumentId::new(p.id),
306 path: p.path.map(std::path::PathBuf::from),
307 version: p.version,
308 text: p.text,
309 },
310 WitEvent::DocumentClosed(id) => NativeEvent::DocumentClosed {
311 id: DocumentId::new(id),
312 },
313 WitEvent::BeforeSave(p) => NativeEvent::BeforeSave {
314 id: DocumentId::new(p.id),
315 path: std::path::PathBuf::from(p.path),
316 },
317 WitEvent::DocumentSaved(p) => NativeEvent::DocumentSaved {
318 id: DocumentId::new(p.id),
319 path: std::path::PathBuf::from(p.path),
320 },
321 WitEvent::DocumentChanged(p) => NativeEvent::DocumentChanged {
322 id: DocumentId::new(p.id),
323 path: p.path.map(std::path::PathBuf::from),
324 version: p.version,
325 edits: p
326 .edits
327 .into_iter()
328 .map(NativeEventAppliedEdit::from_wit)
329 .collect::<Result<Vec<_>, _>>()?,
330 },
331 WitEvent::SelectionsChanged(p) => NativeEvent::SelectionsChanged {
332 id: DocumentId::new(p.id),
333 version: p.version,
334 selections: NativeSelectionSet::from_wit(p.selections)?,
335 },
336 WitEvent::ModalModeChanged(p) => NativeEvent::ModalModeChanged {
337 from: p.from_state,
338 to: p.to_state,
339 },
340 WitEvent::BeforeQuit => NativeEvent::BeforeQuit,
341 WitEvent::OptionChanged(p) => NativeEvent::OptionChanged {
342 name: p.name,
343 old: p.old,
344 new: p.new_value,
345 },
346 WitEvent::MajorEntered(p) => NativeEvent::MajorEntered {
347 buffer: BufferId::new(p.buffer),
348 major: p.mode,
349 },
350 WitEvent::MajorExiting(p) => NativeEvent::MajorExiting {
351 buffer: BufferId::new(p.buffer),
352 major: p.mode,
353 },
354 WitEvent::MinorActivated(p) => NativeEvent::MinorActivated {
355 buffer: BufferId::new(p.buffer),
356 minor: p.mode,
357 },
358 WitEvent::MinorDeactivated(p) => NativeEvent::MinorDeactivated {
359 buffer: BufferId::new(p.buffer),
360 minor: p.mode,
361 },
362 WitEvent::Plugin(p) => NativeEvent::Plugin {
363 name: p.name,
364 payload: p.payload,
365 },
366 WitEvent::PrePluginLoaded(name) => NativeEvent::PrePluginLoaded { name },
367 WitEvent::PluginLoaded(p) => NativeEvent::PluginLoaded {
368 name: p.name,
369 id: p.id,
370 },
371 WitEvent::PluginUnloaded(p) => NativeEvent::PluginUnloaded {
372 name: p.name,
373 id: p.id,
374 },
375 WitEvent::FilesChanged(paths) => NativeEvent::FilesChanged {
381 plugin: 0,
382 paths: paths.into_iter().map(std::path::PathBuf::from).collect(),
383 },
384 })
385 }
386}
387
388fn mode_lifecycle(buffer: &BufferId, mode: &str) -> EventModeLifecycle {
391 EventModeLifecycle {
392 buffer: buffer.raw(),
393 mode: mode.to_string(),
394 }
395}
396
397pub fn project_event_filter(wit: WitEventFilter) -> Result<NativeEventFilter, String> {
405 let kinds = wit
406 .kinds
407 .map(|ks| {
408 ks.into_iter()
409 .map(NativeEventKind::from_wit)
410 .collect::<Result<Vec<_>, _>>()
411 })
412 .transpose()?;
413 Ok(NativeEventFilter {
414 kinds,
415 path_glob: wit.path_globs.map(compile_glob_set),
416 major_modes: wit
417 .major_modes
418 .map(|ms| ms.iter().map(|m| ModeId::new(m)).collect()),
419 minor_modes: wit
420 .minor_modes
421 .map(|ms| ms.iter().map(|m| ModeId::new(m)).collect()),
422 predicate: None,
423 })
424}
425
426#[cfg(test)]
427mod tests {
428 #![allow(clippy::unwrap_used, clippy::panic)]
429
430 use super::*;
431 use lattice_protocol::position::Position;
432 use lattice_protocol::selection::{Selection, SelectionSet};
433
434 fn pos(line: u32, byte: u32) -> Position {
435 Position { line, byte }
436 }
437
438 fn rng(l0: u32, b0: u32, l1: u32, b1: u32) -> NativeRange {
439 NativeRange {
440 start: pos(l0, b0),
441 end: pos(l1, b1),
442 }
443 }
444
445 fn round_trip(ev: NativeEvent) {
449 let wit = ev.to_wit().unwrap();
450 let back = NativeEvent::from_wit(wit).unwrap();
451 assert_eq!(format!("{ev:?}"), format!("{back:?}"));
452 }
453
454 #[test]
455 fn document_lifecycle_arms_round_trip() {
456 round_trip(NativeEvent::DocumentOpened {
457 id: DocumentId::new(7),
458 path: Some(std::path::PathBuf::from("src/lib.rs")),
459 version: 3,
460 text: "fn main() {}\n".into(),
461 });
462 round_trip(NativeEvent::DocumentOpened {
463 id: DocumentId::new(8),
464 path: None, version: 0,
466 text: String::new(),
467 });
468 round_trip(NativeEvent::DocumentClosed {
469 id: DocumentId::new(7),
470 });
471 round_trip(NativeEvent::BeforeSave {
472 id: DocumentId::new(1),
473 path: std::path::PathBuf::from("/tmp/a.rs"),
474 });
475 round_trip(NativeEvent::DocumentSaved {
476 id: DocumentId::new(1),
477 path: std::path::PathBuf::from("/tmp/a.rs"),
478 });
479 }
480
481 #[test]
482 fn document_changed_carries_edits_without_delta() {
483 round_trip(NativeEvent::DocumentChanged {
484 id: DocumentId::new(2),
485 path: Some(std::path::PathBuf::from("x.rs")),
486 version: 4,
487 edits: vec![NativeEventAppliedEdit {
488 original_range: rng(0, 0, 0, 3),
489 inserted_range: rng(0, 0, 0, 5),
490 replaced_text: "abc".into(),
491 inserted_text: "hello".into(),
492 }],
493 });
494 }
495
496 #[test]
497 fn selections_changed_round_trips_the_selection_set() {
498 let set = SelectionSet::from_parts(
499 vec![Selection {
500 anchor: pos(0, 0),
501 head: pos(0, 4),
502 visual: None,
503 }],
504 0,
505 );
506 round_trip(NativeEvent::SelectionsChanged {
507 id: DocumentId::new(3),
508 version: 9,
509 selections: set,
510 });
511 }
512
513 #[test]
514 fn scalar_arms_round_trip() {
515 round_trip(NativeEvent::ModalModeChanged {
516 from: "Normal".into(),
517 to: "Insert".into(),
518 });
519 round_trip(NativeEvent::BeforeQuit);
520 round_trip(NativeEvent::OptionChanged {
521 name: "wrap".into(),
522 old: Some("off".into()),
523 new: "on".into(),
524 });
525 round_trip(NativeEvent::OptionChanged {
526 name: "wrap".into(),
527 old: None, new: "off".into(),
529 });
530 }
531
532 #[test]
533 fn mode_lifecycle_arms_round_trip() {
534 for ev in [
535 NativeEvent::MajorEntered {
536 buffer: BufferId::new(5),
537 major: "rust-mode".into(),
538 },
539 NativeEvent::MajorExiting {
540 buffer: BufferId::new(5),
541 major: "rust-mode".into(),
542 },
543 NativeEvent::MinorActivated {
544 buffer: BufferId::new(5),
545 minor: "diff-mode".into(),
546 },
547 NativeEvent::MinorDeactivated {
548 buffer: BufferId::new(5),
549 minor: "diff-mode".into(),
550 },
551 ] {
552 round_trip(ev);
553 }
554 }
555
556 #[test]
557 fn event_kind_round_trips_every_arm() {
558 for kind in [
559 NativeEventKind::DocumentOpened,
560 NativeEventKind::DocumentClosed,
561 NativeEventKind::BeforeSave,
562 NativeEventKind::DocumentSaved,
563 NativeEventKind::DocumentChanged,
564 NativeEventKind::SelectionsChanged,
565 NativeEventKind::ModalModeChanged,
566 NativeEventKind::BeforeQuit,
567 NativeEventKind::OptionChanged,
568 NativeEventKind::MajorEntered,
569 NativeEventKind::MajorExiting,
570 NativeEventKind::MinorActivated,
571 NativeEventKind::MinorDeactivated,
572 NativeEventKind::Plugin,
573 ] {
574 assert_eq!(
575 NativeEventKind::from_wit(kind.to_wit().unwrap()).unwrap(),
576 kind
577 );
578 }
579 }
580
581 #[test]
582 fn plugin_event_round_trips_opaque_payload() {
583 round_trip(NativeEvent::Plugin {
586 name: "git-gutter.hunks-changed".into(),
587 payload: vec![0x00, 0x91, 0xff, 0x7f, 0xde],
588 });
589 round_trip(NativeEvent::Plugin {
590 name: "empty".into(),
591 payload: Vec::new(),
592 });
593 }
594
595 #[test]
596 fn non_utf8_path_is_a_typed_error() {
597 #[cfg(unix)]
598 {
599 use std::os::unix::ffi::OsStrExt;
600 let bad = std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b"/tmp/\xff.rs"));
601 let ev = NativeEvent::DocumentSaved {
602 id: DocumentId::new(1),
603 path: bad,
604 };
605 assert!(
606 ev.to_wit().is_err(),
607 "non-UTF-8 path must not cross lossily"
608 );
609 }
610 }
611
612 #[test]
613 fn project_event_filter_crosses_declarative_fields() {
614 let wit = WitEventFilter {
615 kinds: Some(vec![WitEventKind::DocumentSaved, WitEventKind::BeforeQuit]),
616 path_globs: Some(vec!["**/*.rs".into()]),
617 major_modes: Some(vec!["rust-mode".into()]),
618 minor_modes: Some(vec!["auto-pair-mode".into()]),
619 };
620 let native = project_event_filter(wit).unwrap();
621 let kinds = native.kinds.expect("kinds crossed");
622 assert_eq!(kinds.len(), 2);
623 assert!(kinds.contains(&NativeEventKind::DocumentSaved));
624 assert!(native.path_glob.is_some(), "path glob compiled");
625 let modes = native.major_modes.expect("major modes crossed");
626 assert_eq!(modes[0].as_str(), "rust-mode");
627 let minors = native.minor_modes.expect("minor modes crossed");
632 assert_eq!(minors[0].as_str(), "auto-pair-mode");
633 assert!(native.predicate.is_none());
635 }
636
637 #[test]
638 fn project_event_filter_wildcard_is_unconstrained() {
639 let wit = WitEventFilter {
640 kinds: None,
641 path_globs: None,
642 major_modes: None,
643 minor_modes: None,
644 };
645 let native = project_event_filter(wit).unwrap();
646 assert!(native.kinds.is_none(), "none kinds stays the wildcard");
647 assert!(native.path_glob.is_none());
648 assert!(native.major_modes.is_none());
649 }
650}