1pub use lattice_picker::picker_sources::*;
16
17#[cfg(test)]
18mod tests {
19 #![allow(clippy::unwrap_used, clippy::panic)]
20
21 use super::*;
22 use crate::app::test_helpers::app_with;
23 use lattice_completion::{CandidateKind, RawCandidate};
30 use lattice_grammar::Args;
31 use lattice_picker::{
32 PickerAcceptOutcome, PickerContext, PickerInitResult, PickerSourceGenerator,
33 PickerSourceSpec, RoutingPayload, SourceResult,
34 };
35 use std::sync::Arc;
36
37 fn empty_reverse() -> Arc<dyn lattice_completion::KeymapReverseLookup> {
43 struct Empty;
44 impl lattice_completion::KeymapReverseLookup for Empty {
45 fn chords_for(&self, _name: &str) -> Vec<lattice_protocol::KeyChord> {
46 Vec::new()
47 }
48 }
49 Arc::new(Empty)
50 }
51
52 fn app_reverse(app: &crate::app::App) -> Arc<dyn lattice_completion::KeymapReverseLookup> {
57 lattice_host::keymap_registry::KeymapReverseLookupHandle::new(
58 &app.editor.keymap,
59 app.editor.registry.clone(),
60 )
61 }
62
63 #[test]
66 fn files_source_inline_init_emits_open_file_routing() {
67 let tmp = std::env::temp_dir().join(format!("lattice-files-src-{}", std::process::id()));
68 let _ = std::fs::remove_dir_all(&tmp);
69 std::fs::create_dir_all(&tmp).unwrap();
70 std::fs::write(tmp.join("a.rs"), "").unwrap();
71 std::fs::write(tmp.join("b.rs"), "").unwrap();
72 let app = app_with("hi\n", 5);
73 let snap = app.ad().snapshot.clone();
74 let ctx = app.build_picker_context(&snap);
75 let source = FilesSource::new();
76 let result = source
77 .init(&ctx, std::slice::from_ref(&tmp.display().to_string()))
78 .expect("inline result");
79 match result {
80 PickerInitResult::Inline(pairs) => {
81 assert_eq!(pairs.len(), 2);
82 for (_cand, routing) in &pairs {
83 match routing {
84 RoutingPayload::OpenFile { path } => {
85 assert!(path.starts_with(std::fs::canonicalize(&tmp).unwrap()));
86 }
87 other => panic!("expected OpenFile, got {other:?}"),
88 }
89 }
90 }
91 other => panic!("expected Inline, got {other:?}"),
92 }
93 let _ = std::fs::remove_dir_all(&tmp);
94 }
95
96 #[test]
104 fn file_pick_source_emits_relative_supplied_values() {
105 use lattice_picker::picker_sources::FilePickSource;
106 let tmp = std::env::temp_dir().join(format!("lattice-file-pick-{}", std::process::id()));
107 let _ = std::fs::remove_dir_all(&tmp);
108 std::fs::create_dir_all(tmp.join("src")).unwrap();
109 std::fs::write(tmp.join("src").join("main.rs"), "").unwrap();
110 let app = app_with("hi\n", 5);
111 let snap = app.ad().snapshot.clone();
112 let ctx = app.build_picker_context(&snap);
113 let source = FilePickSource::new();
114 let result = source
115 .init(&ctx, std::slice::from_ref(&tmp.display().to_string()))
116 .expect("inline result");
117 let PickerInitResult::Inline(pairs) = result else {
118 panic!("expected Inline");
119 };
120 assert_eq!(pairs.len(), 1);
121 match &pairs[0].1 {
122 RoutingPayload::SuppliedValue { value } => {
123 assert_eq!(
124 value,
125 &std::path::PathBuf::from("src")
126 .join("main.rs")
127 .display()
128 .to_string(),
129 "relative to the root, not absolute"
130 );
131 }
132 other => panic!("expected SuppliedValue, got {other:?}"),
133 }
134 let _ = std::fs::remove_dir_all(&tmp);
135 }
136
137 #[test]
142 fn file_pick_candidates_do_not_open_the_file() {
143 use lattice_picker::picker_sources::FilePickSource;
144 let tmp =
145 std::env::temp_dir().join(format!("lattice-file-pick-noopen-{}", std::process::id()));
146 let _ = std::fs::remove_dir_all(&tmp);
147 std::fs::create_dir_all(&tmp).unwrap();
148 std::fs::write(tmp.join("a.rs"), "").unwrap();
149 let app = app_with("hi\n", 5);
150 let snap = app.ad().snapshot.clone();
151 let ctx = app.build_picker_context(&snap);
152 let source = FilePickSource::new();
153 let PickerInitResult::Inline(pairs) = source
154 .init(&ctx, std::slice::from_ref(&tmp.display().to_string()))
155 .expect("inline")
156 else {
157 panic!("expected Inline");
158 };
159 assert!(
160 pairs[0].0.accept_action.is_none(),
161 "a value-supplying candidate must not also open the file"
162 );
163 let _ = std::fs::remove_dir_all(&tmp);
164 }
165
166 #[test]
167 fn file_pick_accept_fills_the_caller() {
168 use lattice_picker::PickerAcceptOutcome;
169 use lattice_picker::picker_sources::FilePickSource;
170 let app = app_with("hi\n", 5);
171 let snap = app.ad().snapshot.clone();
172 let ctx = app.build_picker_context(&snap);
173 let source = FilePickSource::new();
174 let outcome = source
175 .accept(
176 &ctx,
177 &RoutingPayload::SuppliedValue {
178 value: "src/main.rs".into(),
179 },
180 )
181 .expect("accept");
182 match outcome {
183 PickerAcceptOutcome::FillCaller { text } => assert_eq!(text, "src/main.rs"),
184 other => panic!("expected FillCaller, got {other:?}"),
185 }
186 }
187
188 fn ring_ctx_app() -> crate::app::App {
191 app_with("hi\n", 5)
192 }
193
194 #[test]
199 fn yank_ring_lists_the_ring_and_the_registers_together() {
200 use lattice_picker::picker_sources::YankRingSource;
201 let mut app = ring_ctx_app();
202 app.editor.store_yank(
203 lattice_grammar::Register::Unnamed,
204 "from-the-ring".into(),
205 lattice_grammar::effect::YankKind::Charwise,
206 true,
207 );
208 app.editor.store_yank(
209 lattice_grammar::Register::Named('q'),
210 "in-register-q".into(),
211 lattice_grammar::effect::YankKind::Charwise,
212 true,
213 );
214 let snap = app.ad().snapshot.clone();
215 let ctx = app.build_picker_context(&snap);
216 let PickerInitResult::Inline(pairs) =
217 YankRingSource::new().init(&ctx, &[]).expect("inline")
218 else {
219 panic!("expected Inline");
220 };
221 let shown: Vec<String> = pairs.iter().map(|(c, _)| c.display.clone()).collect();
222 assert!(
223 shown.iter().any(|d| d.contains("from-the-ring")),
224 "ring entries missing: {shown:?}"
225 );
226 assert!(
227 shown.iter().any(|d| d.contains("in-register-q")),
228 "register entries missing: {shown:?}"
229 );
230 }
231
232 #[test]
237 fn a_register_row_carries_its_full_contents() {
238 use lattice_picker::picker_sources::YankRingSource;
239 let long = "x".repeat(200);
240 let mut app = ring_ctx_app();
241 app.editor
245 .config
246 .parse_and_set_command("yank.ring.size=0")
247 .expect("settable");
248 app.editor.drain_option_changes();
249 app.editor.store_yank(
250 lattice_grammar::Register::Named('a'),
251 long.clone(),
252 lattice_grammar::effect::YankKind::Charwise,
253 true,
254 );
255 let snap = app.ad().snapshot.clone();
256 let ctx = app.build_picker_context(&snap);
257 let PickerInitResult::Inline(pairs) =
258 YankRingSource::new().init(&ctx, &[]).expect("inline")
259 else {
260 panic!("expected Inline");
261 };
262 let full_rows = pairs
263 .iter()
264 .filter(|(_, r)| matches!(r, RoutingPayload::SuppliedValue { value } if value == &long))
265 .count();
266 assert!(
267 full_rows >= 1,
268 "no row carried the register's full 200-char contents; \
269 values were {:?}",
270 pairs
271 .iter()
272 .map(|(_, r)| match r {
273 RoutingPayload::SuppliedValue { value } => value.chars().count(),
274 _ => 0,
275 })
276 .collect::<Vec<_>>()
277 );
278 }
279
280 #[test]
283 fn a_multi_line_register_keeps_its_newlines() {
284 use lattice_picker::picker_sources::YankRingSource;
285 let mut app = ring_ctx_app();
286 app.editor
287 .config
288 .parse_and_set_command("yank.ring.size=0")
289 .expect("settable");
290 app.editor.drain_option_changes();
291 app.editor.store_yank(
292 lattice_grammar::Register::Named('b'),
293 "one\ntwo".to_string(),
294 lattice_grammar::effect::YankKind::Linewise,
295 true,
296 );
297 let snap = app.ad().snapshot.clone();
298 let ctx = app.build_picker_context(&snap);
299 let PickerInitResult::Inline(pairs) =
300 YankRingSource::new().init(&ctx, &[]).expect("inline")
301 else {
302 panic!("expected Inline");
303 };
304 let has_real_newlines = pairs.iter().any(
305 |(_, r)| matches!(r, RoutingPayload::SuppliedValue { value } if value.contains('\n')),
306 );
307 assert!(
308 has_real_newlines,
309 "a register's newlines must survive into the paste"
310 );
311 }
312
313 #[test]
318 fn a_multi_line_entry_folds_for_display_but_not_for_accept() {
319 use lattice_picker::picker_sources::YankRingSource;
320 let mut app = ring_ctx_app();
321 app.editor.store_yank(
322 lattice_grammar::Register::Unnamed,
323 "first\nsecond\nthird".into(),
324 lattice_grammar::effect::YankKind::Linewise,
325 true,
326 );
327 let snap = app.ad().snapshot.clone();
328 let ctx = app.build_picker_context(&snap);
329 let PickerInitResult::Inline(pairs) =
330 YankRingSource::new().init(&ctx, &[]).expect("inline")
331 else {
332 panic!("expected Inline");
333 };
334 let (cand, routing) = &pairs[0];
335 assert!(
336 !cand.display.contains('\n'),
337 "a picker row is one line; got {:?}",
338 cand.display
339 );
340 match routing {
341 RoutingPayload::SuppliedValue { value } => {
342 assert_eq!(
343 value, "first\nsecond\nthird",
344 "accept carries the real text"
345 );
346 }
347 other => panic!("expected SuppliedValue, got {other:?}"),
348 }
349 }
350
351 #[test]
355 fn the_yank_kind_reaches_the_marginalia() {
356 use lattice_picker::picker_sources::YankRingSource;
357 let mut app = ring_ctx_app();
358 app.editor.store_yank(
359 lattice_grammar::Register::Unnamed,
360 "a line".into(),
361 lattice_grammar::effect::YankKind::Linewise,
362 true,
363 );
364 let snap = app.ad().snapshot.clone();
365 let ctx = app.build_picker_context(&snap);
366 let PickerInitResult::Inline(pairs) =
367 YankRingSource::new().init(&ctx, &[]).expect("inline")
368 else {
369 panic!("expected Inline");
370 };
371 let has_kind = pairs[0].0.annotations.iter().any(|a| match a {
372 lattice_completion::Annotation::Styled { category, segments } => {
373 category.as_ref() == "yank-kind" && segments.iter().any(|s| s.text.contains("line"))
374 }
375 _ => false,
376 });
377 assert!(has_kind, "expected a yank-kind annotation reading `line`");
378 }
379
380 #[test]
381 fn an_empty_ring_says_so_rather_than_opening_blank() {
382 use lattice_picker::picker_sources::YankRingSource;
383 let app = ring_ctx_app();
384 let snap = app.ad().snapshot.clone();
385 let ctx = app.build_picker_context(&snap);
386 let err = YankRingSource::new().init(&ctx, &[]).unwrap_err();
387 assert!(err.contains("nothing has been yanked"), "got {err:?}");
388 }
389
390 #[test]
391 fn yank_ring_accept_fills_the_caller() {
392 use lattice_picker::PickerAcceptOutcome;
393 use lattice_picker::picker_sources::YankRingSource;
394 let app = ring_ctx_app();
395 let snap = app.ad().snapshot.clone();
396 let ctx = app.build_picker_context(&snap);
397 let outcome = YankRingSource::new()
398 .accept(
399 &ctx,
400 &RoutingPayload::SuppliedValue {
401 value: "text".into(),
402 },
403 )
404 .expect("accept");
405 match outcome {
406 PickerAcceptOutcome::FillCaller { text } => assert_eq!(text, "text"),
407 other => panic!("expected FillCaller, got {other:?}"),
408 }
409 }
410
411 #[test]
418 fn files_source_emits_metadata_annotations() {
419 let tmp = std::env::temp_dir().join(format!("lattice-files-margin-{}", std::process::id()));
420 let _ = std::fs::remove_dir_all(&tmp);
421 std::fs::create_dir_all(&tmp).unwrap();
422 std::fs::write(tmp.join("readme.md"), b"# hello").unwrap();
423 let app = app_with("hi\n", 5);
424 let snap = app.ad().snapshot.clone();
425 let ctx = app.build_picker_context(&snap);
426 let source = FilesSource::new();
427 let result = source
428 .init(&ctx, std::slice::from_ref(&tmp.display().to_string()))
429 .expect("inline");
430 let PickerInitResult::Inline(pairs) = result else {
431 panic!("expected Inline");
432 };
433 assert_eq!(pairs.len(), 1);
434 let cand = &pairs[0].0;
435 assert!(cand.display.contains("readme.md"), "got `{}`", cand.display);
437 assert!(
438 !cand.display.contains("minute") && !cand.display.contains("just now"),
439 "metadata must not leak into display: `{}`",
440 cand.display
441 );
442 let cats: Vec<&str> = cand.annotations.iter().map(|a| a.category()).collect();
444 assert_eq!(cats, vec!["perm", "size", "mtime"]);
445 let size = cand
446 .annotations
447 .iter()
448 .find(|a| a.category() == "size")
449 .unwrap();
450 assert_eq!(size.display_text(), "7", "7-byte file → size `7`");
451 let mtime = cand
452 .annotations
453 .iter()
454 .find(|a| a.category() == "mtime")
455 .unwrap();
456 let mt = mtime.display_text();
457 assert!(
458 mt.contains("just now") || mt.contains("minute"),
459 "expected relative mtime, got `{mt}`"
460 );
461 let _ = std::fs::remove_dir_all(&tmp);
462 }
463
464 #[test]
468 fn files_source_empty_root_errors() {
469 let tmp =
470 std::env::temp_dir().join(format!("lattice-files-src-empty-{}", std::process::id()));
471 let _ = std::fs::remove_dir_all(&tmp);
472 std::fs::create_dir_all(&tmp).unwrap();
473 let app = app_with("hi\n", 5);
474 let snap = app.ad().snapshot.clone();
475 let ctx = app.build_picker_context(&snap);
476 let source = FilesSource::new();
477 let err = source
478 .init(&ctx, std::slice::from_ref(&tmp.display().to_string()))
479 .unwrap_err();
480 assert!(err.starts_with("files: no files under"));
481 let _ = std::fs::remove_dir_all(&tmp);
482 }
483
484 #[test]
486 fn recent_source_empty_mru_errors() {
487 let app = app_with("hi\n", 5);
488 let snap = app.ad().snapshot.clone();
489 let ctx = app.build_picker_context(&snap);
490 let source = RecentFilesSource::new();
491 let err = source.init(&ctx, &[]).unwrap_err();
492 assert_eq!(err, "no recent files");
493 }
494
495 #[test]
498 fn recent_source_emits_metadata_annotations() {
499 let mut app = app_with("hi\n", 5);
500 let tmp = std::env::temp_dir().join(format!(
501 "lattice-recent-meta-{}-{:?}",
502 std::process::id(),
503 std::thread::current().id()
504 ));
505 std::fs::write(&tmp, b"hello").unwrap();
506 app.editor.push_recent_file(&tmp);
507 let snap = app.ad().snapshot.clone();
508 let ctx = app.build_picker_context(&snap);
509 let source = RecentFilesSource::new();
510 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
511 panic!("expected Inline");
512 };
513 let row = pairs
514 .iter()
515 .find(|(c, _)| c.display.contains("lattice-recent-meta"))
516 .expect("recent row for the pushed temp file");
517 let cats: Vec<&str> = row.0.annotations.iter().map(|a| a.category()).collect();
518 assert!(
519 cats.contains(&"perm") && cats.contains(&"size") && cats.contains(&"mtime"),
520 "expected perm/size/mtime metadata, got {cats:?}"
521 );
522 let _ = std::fs::remove_file(&tmp);
523 }
524
525 #[test]
530 fn buffers_source_inline_init_floats_active_to_bottom() {
531 let app = app_with("hi\n", 5);
532 let snap = app.ad().snapshot.clone();
533 let ctx = app.build_picker_context(&snap);
534 let source = BuffersSource::new();
535 let result = source.init(&ctx, &[]).expect("inline");
536 let PickerInitResult::Inline(pairs) = result else {
537 panic!("expected Inline");
538 };
539 assert!(!pairs.is_empty());
540 let last = pairs.last().unwrap();
542 match &last.1 {
543 RoutingPayload::Buffer { id } => assert_eq!(*id, ctx.active_buffer.buffer_id),
544 other => panic!("expected Buffer routing, got {other:?}"),
545 }
546 let cand = &last.0;
547 assert!(!cand.display.contains("(current)"));
549 assert!(!cand.display.contains("[+]"));
550 let cat = |c: &str| {
551 cand.annotations
552 .iter()
553 .find(|a| a.category() == c)
554 .map(|a| a.display_text().into_owned())
555 };
556 assert_eq!(
559 cat("buffer-id"),
560 Some(format!("#{}", ctx.active_buffer.buffer_id))
561 );
562 assert!(cat("kind").is_some(), "kind cell missing");
563 assert!(
564 cat("status").is_some_and(|s| s.contains('•')),
565 "active row missing active-status marker, got {:?}",
566 cat("status")
567 );
568 }
569
570 #[test]
576 fn buffers_source_candidates_carry_typed_accept_action() {
577 use lattice_completion::{AcceptAction, AcceptHandler, DefaultAcceptHandler};
578
579 let app = app_with("hi\n", 5);
580 let snap = app.ad().snapshot.clone();
581 let ctx = app.build_picker_context(&snap);
582 let source = BuffersSource::new();
583 let result = source.init(&ctx, &[]).expect("inline");
584 let PickerInitResult::Inline(pairs) = result else {
585 panic!("expected Inline");
586 };
587 for (cand, routing) in &pairs {
588 let action = cand
591 .accept_action
592 .as_deref()
593 .expect("BuffersSource sets accept_action on every candidate");
594 let RoutingPayload::Buffer { id: routing_id } = routing else {
595 panic!("expected Buffer routing");
596 };
597 match action {
598 AcceptAction::SwitchBuffer { id } => assert_eq!(id.0, *routing_id),
599 other => panic!("expected SwitchBuffer, got {other:?}"),
600 }
601 let resolved = DefaultAcceptHandler.accept(cand).expect("handler ok");
603 assert_eq!(&resolved, action);
604 }
605 }
606
607 #[test]
608 fn first_party_generators_returns_all_built_in_sources() {
609 let app = app_with("hi\n", 5);
610 let generators = first_party_generators(
611 app.editor.registry.clone(),
612 app.editor.config.clone(),
613 empty_reverse(),
614 None,
615 );
616 let ids: Vec<&str> = generators.iter().map(|g| g.spec().id.as_ref()).collect();
617 assert_eq!(
618 ids,
619 vec![
620 "files",
621 "file-pick",
632 "dir-pick",
633 "yank-ring",
635 "recent",
636 "buffers",
637 "lines",
638 "jumps",
639 "commands",
640 "history",
641 "search-history",
646 "pane-buffer-history",
650 "registers",
651 "marks",
652 "grep",
653 "outline",
654 ]
655 );
656 }
657
658 #[test]
662 fn jumps_source_empty_history_errors() {
663 let app = app_with("hi\n", 5);
664 let snap = app.ad().snapshot.clone();
665 let ctx = app.build_picker_context(&snap);
666 let source = JumpsSource::new();
667 let err = source.init(&ctx, &[]).unwrap_err();
668 assert!(err.contains("position history is empty"), "got {err}");
669 }
670
671 #[test]
677 fn commands_source_emits_ex_commands_only() {
678 let app = app_with("hi\n", 5);
679 let snap = app.ad().snapshot.clone();
680 let ctx = app.build_picker_context(&snap);
681 let source = CommandsSource::new(app.editor.registry.clone(), empty_reverse());
682 let result = source.init(&ctx, &[]).expect("inline");
683 let PickerInitResult::Inline(pairs) = result else {
684 panic!("expected Inline");
685 };
686 assert!(!pairs.is_empty(), "should have at least one command");
687 for (cand, routing) in &pairs {
690 match routing {
691 RoutingPayload::InvokeCommand { id, .. } => {
692 assert!(!id.is_empty());
698 }
699 other => panic!("expected InvokeCommand, got {other:?}"),
700 }
701 assert!(!cand.text.starts_with("ex:"), "got {}", cand.text);
704 }
705 let texts: Vec<&str> = pairs.iter().map(|(c, _)| c.text.as_str()).collect();
707 let mut sorted = texts.clone();
708 sorted.sort();
709 assert_eq!(texts, sorted);
710 }
711
712 #[test]
717 fn commands_source_emits_marginalia_annotations() {
718 use lattice_completion::Annotation;
719 let app = app_with("hi\n", 5);
720 let snap = app.ad().snapshot.clone();
721 let ctx = app.build_picker_context(&snap);
722 let source = CommandsSource::new(app.editor.registry.clone(), empty_reverse());
723 let result = source.init(&ctx, &[]).expect("inline");
724 let PickerInitResult::Inline(pairs) = result else {
725 panic!("expected Inline");
726 };
727 let write_row = pairs
728 .iter()
729 .find(|(c, _)| c.text == "write")
730 .expect("write command row");
731 let cand = &write_row.0;
732 assert_eq!(cand.display, "write");
734 let by_cat = |cat: &str| {
735 cand.annotations
736 .iter()
737 .find(|a| a.category() == cat)
738 .map(|a| a.display_text().into_owned())
739 };
740 assert_eq!(by_cat("args").as_deref(), Some("[<path>]"));
742 assert_eq!(by_cat("latency").as_deref(), Some("[display]"));
744 assert!(
746 by_cat("doc").is_some_and(|d| d.contains("Write")),
747 "expected doc annotation containing `Write`, got {:?}",
748 by_cat("doc")
749 );
750 assert!(
752 cand.annotations
753 .iter()
754 .all(|a| matches!(a, Annotation::Styled { .. } | Annotation::DocSnippet(_)))
755 );
756 }
757
758 #[test]
764 fn commands_source_emits_keybinding_annotation() {
765 use lattice_completion::Annotation;
766 use lattice_protocol::KeyChord;
767
768 struct Stub;
769 impl lattice_completion::KeymapReverseLookup for Stub {
770 fn chords_for(&self, name: &str) -> Vec<KeyChord> {
771 if name == "ex:write" {
773 vec![KeyChord::ctrl('s')]
774 } else {
775 Vec::new()
776 }
777 }
778 }
779
780 let app = app_with("hi\n", 5);
781 let snap = app.ad().snapshot.clone();
782 let ctx = app.build_picker_context(&snap);
783 let source = CommandsSource::new(app.editor.registry.clone(), Arc::new(Stub));
784 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
785 panic!("expected Inline");
786 };
787
788 let keybinding_of = |name: &str| -> Option<String> {
789 pairs
790 .iter()
791 .find(|(c, _)| c.text == name)
792 .expect("row present")
793 .0
794 .annotations
795 .iter()
796 .find(|a| matches!(a, Annotation::Keybinding(_)))
797 .map(|a| a.display_text().into_owned())
798 };
799
800 let write_kb = keybinding_of("write").expect("write has a keybinding annotation");
803 assert!(write_kb.contains("C-s"), "got {write_kb:?}");
804
805 assert_eq!(
808 keybinding_of("quit"),
809 None,
810 "unbound command must not push an empty keybinding annotation"
811 );
812 }
813
814 #[test]
820 fn commands_source_keybinding_from_real_keymap() {
821 use lattice_completion::Annotation;
822
823 let app = app_with("hi\n", 5);
824 let snap = app.ad().snapshot.clone();
825 let ctx = app.build_picker_context(&snap);
826 let source = CommandsSource::new(app.editor.registry.clone(), app_reverse(&app));
827 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
828 panic!("expected Inline");
829 };
830
831 let help_kb = pairs
832 .iter()
833 .find(|(c, _)| c.text == "help")
834 .expect("help row present")
835 .0
836 .annotations
837 .iter()
838 .find_map(|a| match a {
839 Annotation::Keybinding(_) => Some(a.display_text().into_owned()),
840 _ => None,
841 })
842 .expect("help carries a keybinding from the real keymap");
843 assert!(help_kb.contains("C-h"), "got {help_kb:?}");
844 }
845
846 #[test]
850 fn commands_source_accept_translates_invoke_command() {
851 let app = app_with("hi\n", 5);
852 let snap = app.ad().snapshot.clone();
853 let ctx = app.build_picker_context(&snap);
854 let source = CommandsSource::new(app.editor.registry.clone(), empty_reverse());
855 let routing = RoutingPayload::InvokeCommand {
856 id: "ex:write".into(),
857 args: Args::None,
858 };
859 let outcome = source.accept(&ctx, &routing).expect("ok");
860 match outcome {
861 PickerAcceptOutcome::InvokeCommand { id, args } => {
862 assert_eq!(id, "ex:write");
863 assert!(matches!(args, Args::None));
864 }
865 other => panic!("expected InvokeCommand, got {other:?}"),
866 }
867 let bad = RoutingPayload::OpenFile {
868 path: "/tmp/x".into(),
869 };
870 assert!(source.accept(&ctx, &bad).is_err());
871 }
872
873 #[test]
876 fn registers_source_empty_errors() {
877 let app = app_with("hi\n", 5);
878 let snap = app.ad().snapshot.clone();
879 let ctx = app.build_picker_context(&snap);
880 let source = RegistersSource::new();
881 let err = source.init(&ctx, &[]).unwrap_err();
882 assert!(err.contains("no registers set"), "got {err}");
883 }
884
885 #[test]
889 fn registers_source_emits_paste_routing() {
890 let app = app_with("hi\n", 5);
891 let snap = app.ad().snapshot.clone();
892 let mut ctx = app.build_picker_context(&snap);
893 ctx.registers = vec![("\"".into(), "hello".into()), ("a".into(), "world".into())];
894 let source = RegistersSource::new();
895 let result = source.init(&ctx, &[]).expect("inline");
896 let PickerInitResult::Inline(pairs) = result else {
897 panic!("expected Inline");
898 };
899 assert_eq!(pairs.len(), 2);
900 match &pairs[0].1 {
901 RoutingPayload::PasteRegister { name } => assert_eq!(*name, '"'),
902 other => panic!("expected PasteRegister, got {other:?}"),
903 }
904 match &pairs[1].1 {
905 RoutingPayload::PasteRegister { name } => assert_eq!(*name, 'a'),
906 other => panic!("expected PasteRegister, got {other:?}"),
907 }
908 assert_eq!(pairs[1].0.display, "world");
911 let reg = pairs[1]
912 .0
913 .annotations
914 .iter()
915 .find(|a| a.category() == "register")
916 .expect("register cell");
917 assert_eq!(reg.display_text(), "\"a");
918 }
919
920 #[test]
923 fn registers_source_accept_translates_paste_register() {
924 let app = app_with("hi\n", 5);
925 let snap = app.ad().snapshot.clone();
926 let ctx = app.build_picker_context(&snap);
927 let source = RegistersSource::new();
928 let routing = RoutingPayload::PasteRegister { name: 'a' };
929 match source.accept(&ctx, &routing).expect("ok") {
930 PickerAcceptOutcome::PasteRegister { name } => assert_eq!(name, 'a'),
931 other => panic!("expected PasteRegister outcome, got {other:?}"),
932 }
933 let bad = RoutingPayload::OpenFile {
934 path: "/tmp/x".into(),
935 };
936 assert!(source.accept(&ctx, &bad).is_err());
937 }
938
939 #[test]
942 fn history_source_empty_errors() {
943 let app = app_with("hi\n", 5);
944 let snap = app.ad().snapshot.clone();
945 let ctx = app.build_picker_context(&snap);
946 let source = CommandHistorySource::new();
947 let err = source.init(&ctx, &[]).unwrap_err();
948 assert!(err.contains("no command-line history"), "got {err}");
949 }
950
951 #[test]
955 fn history_source_emits_load_command_line_newest_first() {
956 let app = app_with("hi\n", 5);
957 let snap = app.ad().snapshot.clone();
958 let mut ctx = app.build_picker_context(&snap);
959 ctx.command_history = vec![
961 "set number".into(),
962 "write".into(),
963 "edit src/main.rs".into(),
964 ];
965 let source = CommandHistorySource::new();
966 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
967 panic!("expected Inline");
968 };
969 assert_eq!(pairs.len(), 3);
970 assert_eq!(pairs[0].0.display, "edit src/main.rs");
972 assert_eq!(pairs[2].0.display, "set number");
973 match &pairs[0].1 {
974 RoutingPayload::LoadCommandLine { text } => assert_eq!(text, "edit src/main.rs"),
975 other => panic!("expected LoadCommandLine, got {other:?}"),
976 }
977 }
978
979 #[test]
983 fn history_source_accept_translates_load_command_line() {
984 let app = app_with("hi\n", 5);
985 let snap = app.ad().snapshot.clone();
986 let ctx = app.build_picker_context(&snap);
987 let source = CommandHistorySource::new();
988 let routing = RoutingPayload::LoadCommandLine {
989 text: "write".into(),
990 };
991 match source.accept(&ctx, &routing).expect("ok") {
992 PickerAcceptOutcome::LoadCommandLine { text } => assert_eq!(text, "write"),
993 other => panic!("expected LoadCommandLine outcome, got {other:?}"),
994 }
995 let bad = RoutingPayload::OpenFile {
996 path: "/tmp/x".into(),
997 };
998 assert!(source.accept(&ctx, &bad).is_err());
999 }
1000
1001 #[test]
1008 fn grep_source_empty_args_returns_empty_inline() {
1009 let app = app_with("hi\n", 5);
1010 let snap = app.ad().snapshot.clone();
1011 let ctx = app.build_picker_context(&snap);
1012 let source = GrepSource::new(app.editor.config.clone(), None);
1013 let result = source
1014 .init(&ctx, &[])
1015 .expect("init must not error on no args");
1016 match result {
1017 lattice_picker::PickerInitResult::Inline(pairs) => assert!(pairs.is_empty()),
1018 other => panic!("expected Inline(empty), got {other:?}"),
1019 }
1020 let result = source
1021 .init(&ctx, &[String::new()])
1022 .expect("init must not error on empty arg");
1023 match result {
1024 lattice_picker::PickerInitResult::Inline(pairs) => assert!(pairs.is_empty()),
1025 other => panic!("expected Inline(empty) for empty arg, got {other:?}"),
1026 }
1027 }
1028
1029 #[test]
1033 fn grep_source_on_query_changed_empty_short_circuits() {
1034 let app = app_with("hi\n", 5);
1035 let snap = app.ad().snapshot.clone();
1036 let ctx = app.build_picker_context(&snap);
1037 let source = GrepSource::new(app.editor.config.clone(), None);
1038 let result = source
1039 .on_query_changed(&ctx, "")
1040 .expect("live source returns Some")
1041 .expect("no error");
1042 match result {
1043 lattice_picker::PickerInitResult::Inline(pairs) => assert!(pairs.is_empty()),
1044 other => panic!("expected Inline(empty), got {other:?}"),
1045 }
1046 let result = source
1048 .on_query_changed(&ctx, " ")
1049 .expect("live source returns Some")
1050 .expect("no error");
1051 match result {
1052 lattice_picker::PickerInitResult::Inline(pairs) => assert!(pairs.is_empty()),
1053 other => panic!("expected Inline(empty) for whitespace query, got {other:?}"),
1054 }
1055 }
1056
1057 #[test]
1062 fn grep_source_spec_is_live() {
1063 let app = app_with("hi\n", 5);
1064 let source = GrepSource::new(app.editor.config.clone(), None);
1065 assert!(source.spec().live, "GrepSource must declare live = true");
1066 }
1067
1068 #[test]
1072 fn grep_source_unknown_backend_errors() {
1073 let app = app_with("hi\n", 5);
1074 app.editor
1075 .config
1076 .parse_and_set_command("picker.grep.backend=definitely-not-a-binary")
1077 .unwrap();
1078 let snap = app.ad().snapshot.clone();
1079 let ctx = app.build_picker_context(&snap);
1080 let source = GrepSource::new(app.editor.config.clone(), None);
1081 let err = source.init(&ctx, &["TODO".to_string()]).unwrap_err();
1082 assert!(err.contains("definitely-not-a-binary"), "got {err}");
1083 assert!(err.contains("not found on PATH"), "got {err}");
1084 }
1085
1086 struct DelayedFutureSource {
1093 spec: PickerSourceSpec,
1094 }
1095
1096 impl DelayedFutureSource {
1097 fn new() -> Self {
1098 Self {
1099 spec: PickerSourceSpec::no_args(
1100 "delayed-test",
1101 "Test-only async source that resolves to one OpenFile candidate.",
1102 ),
1103 }
1104 }
1105 }
1106
1107 impl PickerSourceGenerator for DelayedFutureSource {
1108 fn spec(&self) -> &PickerSourceSpec {
1109 &self.spec
1110 }
1111
1112 fn init(
1113 &self,
1114 _ctx: &PickerContext<'_>,
1115 _args: &[String],
1116 ) -> SourceResult<PickerInitResult> {
1117 let fut = Box::pin(async move {
1118 tokio::task::yield_now().await;
1122 let cand = RawCandidate::plain(String::from("test-result"), CandidateKind::Plain);
1123 Ok(vec![(
1124 cand,
1125 RoutingPayload::OpenFile {
1126 path: "/tmp/lattice-test-future".into(),
1127 },
1128 )])
1129 });
1130 Ok(PickerInitResult::Future(fut))
1131 }
1132
1133 fn accept(
1134 &self,
1135 _ctx: &PickerContext<'_>,
1136 _routing: &RoutingPayload,
1137 ) -> SourceResult<PickerAcceptOutcome> {
1138 Ok(PickerAcceptOutcome::NoOp)
1139 }
1140 }
1141
1142 #[test]
1148 fn async_init_seat_path_pumps_future_result_into_picker() {
1149 use std::time::Duration;
1150
1151 let mut app = app_with("hi\n", 5);
1152 let mut reg = lattice_picker::PickerRegistry::new();
1156 let source: Arc<dyn PickerSourceGenerator> = Arc::new(DelayedFutureSource::new());
1157 reg.register_generator(source);
1158 app.editor.picker_registry.store(Arc::new(reg));
1159 app.open_picker("delayed-test".into(), Vec::new(), None, None);
1162 assert!(
1163 app.editor.picker.is_none(),
1164 "picker shouldn't seat sync on Future"
1165 );
1166 assert!(
1167 app.editor.pending_picker_init.is_some(),
1168 "pending should be set"
1169 );
1170 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1174 while std::time::Instant::now() < deadline {
1175 app.drain_pending_picker_init();
1176 if app.editor.picker.is_some() {
1177 break;
1178 }
1179 std::thread::sleep(Duration::from_millis(10));
1180 }
1181 let p = app
1182 .editor
1183 .picker
1184 .as_ref()
1185 .expect("picker seated after drain");
1186 assert_eq!(p.candidates.len(), 1);
1187 assert_eq!(p.source_id.as_deref(), Some("delayed-test"));
1188 }
1189
1190 #[test]
1201 fn esc_cancels_a_picker_that_has_not_finished_loading() {
1202 use std::time::Duration;
1203
1204 let mut app = app_with("hi\n", 5);
1205 let mut reg = lattice_picker::PickerRegistry::new();
1206 let source: Arc<dyn PickerSourceGenerator> = Arc::new(DelayedFutureSource::new());
1207 reg.register_generator(source);
1208 app.editor.picker_registry.store(Arc::new(reg));
1209
1210 app.open_picker("delayed-test".into(), Vec::new(), None, None);
1211 assert!(app.editor.picker.is_none(), "parked, not seated");
1212 assert!(app.editor.pending_picker_init.is_some(), "future in flight");
1213 assert!(
1217 app.ad().picker_open,
1218 "a parked picker must still report open, or `<Esc>` never \
1219 reaches the picker router at all"
1220 );
1221
1222 app.apply(crate::Action::PickerDismiss);
1223 assert!(
1224 app.editor.pending_picker_init.is_none(),
1225 "dismiss must cancel the in-flight init"
1226 );
1227
1228 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1232 while std::time::Instant::now() < deadline {
1233 app.drain_pending_picker_init();
1234 assert!(
1235 app.editor.picker.is_none(),
1236 "a cancelled picker must never seat"
1237 );
1238 std::thread::sleep(Duration::from_millis(10));
1239 }
1240 }
1241
1242 #[test]
1246 fn outline_source_no_symbols_errors() {
1247 let app = app_with("hi\n", 5);
1248 let snap = app.ad().snapshot.clone();
1249 let ctx = app.build_picker_context(&snap);
1250 let source = OutlineSource::new();
1251 let err = source.init(&ctx, &[]).unwrap_err();
1252 assert!(err.starts_with("outline:"), "got {err}");
1253 }
1254
1255 #[test]
1259 fn outline_source_emits_jump_in_buffer_routing() {
1260 let app = app_with("hi\n", 5);
1261 let snap = app.ad().snapshot.clone();
1262 let mut ctx = app.build_picker_context(&snap);
1263 let active_id = ctx.active_buffer.buffer_id;
1264 ctx.active_buffer.syntax_symbols =
1265 vec![("foo".to_string(), 2, 4), ("bar".to_string(), 10, 0)];
1266 let source = OutlineSource::new();
1267 let result = source.init(&ctx, &[]).expect("inline");
1268 let PickerInitResult::Inline(pairs) = result else {
1269 panic!("expected Inline");
1270 };
1271 assert_eq!(pairs.len(), 2);
1272 match &pairs[0].1 {
1273 RoutingPayload::JumpInBuffer {
1274 buffer_id,
1275 line,
1276 col,
1277 } => {
1278 assert_eq!(*buffer_id, active_id);
1279 assert_eq!(*line, 2);
1280 assert_eq!(*col, 4);
1281 }
1282 other => panic!("expected JumpInBuffer, got {other:?}"),
1283 }
1284 assert_eq!(pairs[0].0.display, "foo");
1287 let loc = pairs[0]
1288 .0
1289 .annotations
1290 .iter()
1291 .find(|a| a.category() == "location")
1292 .expect("location cell");
1293 assert_eq!(loc.display_text(), "3");
1294 }
1295
1296 #[test]
1302 fn lines_source_emits_display_spans_for_parsed_buffer() {
1303 let src = "fn foo() {}\n";
1304 let mut app = app_with(src, 5);
1305 let mut syn = lattice_syntax::Syntax::for_language(lattice_syntax::Lang::Rust)
1308 .unwrap()
1309 .expect("rust syntax");
1310 syn.parse_at(src, 0);
1311 app.editor.syntax = Some(lattice_syntax::SyntaxHandle::seeded(syn));
1312 let snap = app.ad().snapshot.clone();
1313 let ctx = app.build_picker_context(&snap);
1314 let source = LinesSource::new();
1315 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
1316 panic!("expected Inline");
1317 };
1318 let first = &pairs[0].0;
1319 assert_eq!(first.display, "fn foo() {}");
1320 assert!(
1321 !first.display_spans.is_empty(),
1322 "a parsed code line should carry syntax spans"
1323 );
1324 assert!(
1326 first
1327 .display_spans
1328 .iter()
1329 .all(|s| s.range.end <= first.display.len()),
1330 "spans must be clipped to the display run"
1331 );
1332 assert!(
1334 first
1335 .display_spans
1336 .iter()
1337 .any(|s| s.style == lattice_cells::style::Style::Keyword),
1338 "expected a keyword-styled span for `fn`"
1339 );
1340 }
1341
1342 #[test]
1345 fn lines_source_no_grammar_emits_no_display_spans() {
1346 let app = app_with("fn foo() {}\n", 5); let snap = app.ad().snapshot.clone();
1348 let ctx = app.build_picker_context(&snap);
1349 let source = LinesSource::new();
1350 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
1351 panic!("expected Inline");
1352 };
1353 assert!(
1354 pairs.iter().all(|(c, _)| c.display_spans.is_empty()),
1355 "no grammar → plain preview"
1356 );
1357 }
1358
1359 #[test]
1365 fn outline_source_projects_display_spans_onto_symbol_name() {
1366 let app = app_with("fn foo() {}\n", 5);
1367 let snap = app.ad().snapshot.clone();
1368 let mut ctx = app.build_picker_context(&snap);
1369 ctx.active_buffer.syntax_symbols = vec![("foo".to_string(), 0, 3)];
1371 ctx.active_buffer.syntax_highlights = vec![vec![
1373 lattice_completion::DisplaySpan {
1374 range: 0..2,
1375 style: lattice_cells::style::Style::Keyword,
1376 },
1377 lattice_completion::DisplaySpan {
1378 range: 3..6,
1379 style: lattice_cells::style::Style::Function,
1380 },
1381 ]];
1382 let source = OutlineSource::new();
1383 let PickerInitResult::Inline(pairs) = source.init(&ctx, &[]).expect("inline") else {
1384 panic!("expected Inline");
1385 };
1386 let foo = &pairs[0].0;
1387 assert_eq!(foo.display, "foo");
1388 assert_eq!(foo.display_spans.len(), 1);
1391 assert_eq!(foo.display_spans[0].range, 0..3);
1392 assert_eq!(
1393 foo.display_spans[0].style,
1394 lattice_cells::style::Style::Function
1395 );
1396 }
1397
1398 #[test]
1400 fn marks_source_empty_errors() {
1401 let app = app_with("hi\n", 5);
1402 let snap = app.ad().snapshot.clone();
1403 let ctx = app.build_picker_context(&snap);
1404 let source = MarksSource::new();
1405 let err = source.init(&ctx, &[]).unwrap_err();
1406 assert!(err.contains("no marks set"), "got {err}");
1407 }
1408
1409 #[test]
1412 fn marks_source_emits_jump_to_mark_routing() {
1413 use lattice_protocol::Position;
1414
1415 let app = app_with("hi\n", 5);
1416 let snap = app.ad().snapshot.clone();
1417 let mut ctx = app.build_picker_context(&snap);
1418 ctx.marks = vec![('a', Position::new(2, 0)), ('b', Position::new(5, 3))];
1419 let source = MarksSource::new();
1420 let result = source.init(&ctx, &[]).expect("inline");
1421 let PickerInitResult::Inline(pairs) = result else {
1422 panic!("expected Inline");
1423 };
1424 assert_eq!(pairs.len(), 2);
1425 match &pairs[0].1 {
1426 RoutingPayload::JumpToMark { name } => assert_eq!(*name, 'a'),
1427 other => panic!("expected JumpToMark, got {other:?}"),
1428 }
1429 let loc = |i: usize| {
1432 pairs[i]
1433 .0
1434 .annotations
1435 .iter()
1436 .find(|a| a.category() == "location")
1437 .map(|a| a.display_text().into_owned())
1438 };
1439 assert_eq!(pairs[0].0.display, "'a");
1440 assert_eq!(loc(0).as_deref(), Some("3:1"));
1441 assert_eq!(pairs[1].0.display, "'b");
1442 assert_eq!(loc(1).as_deref(), Some("6:4"));
1443 }
1444
1445 #[test]
1448 fn marks_source_accept_translates_jump_to_mark() {
1449 let app = app_with("hi\n", 5);
1450 let snap = app.ad().snapshot.clone();
1451 let ctx = app.build_picker_context(&snap);
1452 let source = MarksSource::new();
1453 let routing = RoutingPayload::JumpToMark { name: 'm' };
1454 match source.accept(&ctx, &routing).expect("ok") {
1455 PickerAcceptOutcome::JumpToMark { name } => assert_eq!(name, 'm'),
1456 other => panic!("expected JumpToMark outcome, got {other:?}"),
1457 }
1458 let bad = RoutingPayload::OpenFile {
1459 path: "/tmp/x".into(),
1460 };
1461 assert!(source.accept(&ctx, &bad).is_err());
1462 }
1463
1464 #[test]
1471 fn jumps_source_emits_newest_first_with_source_tags() {
1472 use lattice_picker::{PositionEntry, PositionSource};
1473
1474 let app = app_with("hi\n", 5);
1475 let snap = app.ad().snapshot.clone();
1476 let mut ctx = app.build_picker_context(&snap);
1477 ctx.position_history = vec![
1478 PositionEntry {
1479 buffer_id: 1,
1480 line: 0,
1481 col: 0,
1482 source: PositionSource::AutoJump,
1483 },
1484 PositionEntry {
1485 buffer_id: 1,
1486 line: 5,
1487 col: 2,
1488 source: PositionSource::NamedMark('a'),
1489 },
1490 PositionEntry {
1491 buffer_id: 2,
1492 line: 10,
1493 col: 0,
1494 source: PositionSource::PluginPush,
1495 },
1496 ];
1497 let source = JumpsSource::new();
1498 let result = source.init(&ctx, &[]).expect("inline");
1499 let PickerInitResult::Inline(pairs) = result else {
1500 panic!("expected Inline");
1501 };
1502 assert_eq!(pairs.len(), 3);
1503 match &pairs[0].1 {
1505 RoutingPayload::JumpInBuffer {
1506 buffer_id,
1507 line,
1508 col,
1509 } => {
1510 assert_eq!(*buffer_id, 2);
1511 assert_eq!(*line, 10);
1512 assert_eq!(*col, 0);
1513 }
1514 other => panic!("expected JumpInBuffer, got {other:?}"),
1515 }
1516 let source_tag = |i: usize| {
1518 pairs[i]
1519 .0
1520 .annotations
1521 .iter()
1522 .find(|a| a.category() == "source")
1523 .map(|a| a.display_text().into_owned())
1524 };
1525 assert_eq!(source_tag(1).as_deref(), Some("'a"));
1526 assert_eq!(source_tag(2).as_deref(), Some("auto"));
1527 assert!(
1529 pairs[0]
1530 .0
1531 .annotations
1532 .iter()
1533 .any(|a| a.category() == "location")
1534 );
1535 }
1536
1537 #[test]
1541 fn lines_source_emits_row_per_line() {
1542 let app = app_with("alpha\nbeta\ngamma\n", 10);
1543 let snap = app.ad().snapshot.clone();
1544 let ctx = app.build_picker_context(&snap);
1545 let active_id = ctx.active_buffer.buffer_id;
1546 let source = LinesSource::new();
1547 let result = source.init(&ctx, &[]).expect("inline");
1548 let PickerInitResult::Inline(pairs) = result else {
1549 panic!("expected Inline");
1550 };
1551 assert_eq!(pairs.len(), 3);
1553 for (i, (cand, routing)) in pairs.iter().enumerate() {
1554 match routing {
1555 RoutingPayload::JumpInBuffer {
1556 buffer_id,
1557 line,
1558 col,
1559 } => {
1560 assert_eq!(*buffer_id, active_id);
1561 assert_eq!(*line, i as u32);
1562 assert_eq!(*col, 0);
1563 }
1564 other => panic!("expected JumpInBuffer, got {other:?}"),
1565 }
1566 let loc = cand
1569 .annotations
1570 .iter()
1571 .find(|a| a.category() == "location")
1572 .map(|a| a.display_text().into_owned());
1573 assert_eq!(loc.as_deref(), Some((i + 1).to_string().as_str()));
1574 }
1575 assert_eq!(pairs[0].0.display, "alpha");
1577 assert_eq!(pairs[2].0.display, "gamma");
1578 }
1579
1580 #[test]
1584 fn lines_source_empty_buffer_errors() {
1585 let app = app_with("", 5);
1586 let snap = app.ad().snapshot.clone();
1587 let ctx = app.build_picker_context(&snap);
1595 let source = LinesSource::new();
1596 let result = source.init(&ctx, &[]).expect("inline");
1597 if let PickerInitResult::Inline(pairs) = result {
1598 assert_eq!(pairs.len(), 1);
1600 } else {
1601 panic!("expected Inline");
1602 }
1603 }
1604
1605 #[test]
1609 fn lines_source_accept_translates_jump_in_buffer() {
1610 let app = app_with("hi\n", 5);
1611 let snap = app.ad().snapshot.clone();
1612 let ctx = app.build_picker_context(&snap);
1613 let source = LinesSource::new();
1614 let routing = RoutingPayload::JumpInBuffer {
1615 buffer_id: 7,
1616 line: 12,
1617 col: 3,
1618 };
1619 let outcome = source.accept(&ctx, &routing).expect("ok");
1620 match outcome {
1621 PickerAcceptOutcome::JumpInBuffer {
1622 buffer_id,
1623 line,
1624 col,
1625 } => {
1626 assert_eq!(buffer_id, 7);
1627 assert_eq!(line, 12);
1628 assert_eq!(col, 3);
1629 }
1630 other => panic!("expected JumpInBuffer, got {other:?}"),
1631 }
1632 let bad = RoutingPayload::OpenFile {
1633 path: "/tmp/x".into(),
1634 };
1635 assert!(source.accept(&ctx, &bad).is_err());
1636 }
1637}