1use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6use std::{
7 borrow::Cow,
8 mem::take,
9 sync::{
10 Arc,
11 atomic::{self, AtomicU32},
12 },
13};
14use unicode_segmentation::UnicodeSegmentation;
15use unicode_width::UnicodeWidthStr;
16
17use super::{injector::WorkerInjector, query::PickerQuery};
18use crate::{
19 SSS,
20 config::AutoscrollSettings,
21 nucleo::Render,
22 utils::text::{hscroll_indicator, text_to_string, wrap_text, wrapping_indicator},
23};
24
25type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
26pub struct Column<T> {
27 pub name: Arc<str>,
28 pub(super) format: ColumnFormatFn<T>,
29 pub(super) filter: bool,
31}
32
33impl<T> Column<T> {
34 pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
35 Self {
36 name: name.into(),
37 format,
38 filter: true,
39 }
40 }
41
42 pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
43 where
44 F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
45 {
46 Self {
47 name: name.into(),
48 format: Box::new(f),
49 filter: true,
50 }
51 }
52
53 pub fn without_filtering(mut self) -> Self {
55 self.filter = false;
56 self
57 }
58
59 pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
60 (self.format)(item)
61 }
62
63 pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
65 Cow::Owned(text_to_string(&(self.format)(item)))
66 }
67}
68
69pub struct Worker<T>
73where
74 T: SSS,
75{
76 pub nucleo: nucleo::Nucleo<T>,
78 pub query: PickerQuery,
80 pub col_indices_buffer: Vec<u32>,
83 pub columns: Arc<[Column<T>]>,
84
85 pub(super) version: Arc<AtomicU32>,
87 column_options: Vec<ColumnOptions>,
89}
90
91bitflags! {
97 #[derive(Default, Clone, Debug)]
98 pub struct ColumnOptions: u8 {
99 const Optional = 1 << 0;
100 const OrUseDefault = 1 << 2;
101 }
102}
103
104impl<T> Worker<T>
105where
106 T: SSS,
107{
108 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
110 let columns: Arc<[_]> = columns.into_iter().collect();
111 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
112
113 let inner = nucleo::Nucleo::new(
114 nucleo::Config::DEFAULT,
115 Arc::new(|| {}),
116 None,
117 matcher_columns,
118 );
119
120 Self {
121 nucleo: inner,
122 col_indices_buffer: Vec::with_capacity(128),
123 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
124 column_options: vec![ColumnOptions::default(); columns.len()],
125 columns,
126 version: Arc::new(AtomicU32::new(0)),
127 }
128 }
129
130 #[cfg(feature = "experimental")]
131 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
132 if options.contains(ColumnOptions::Optional) {
133 self.nucleo
134 .pattern
135 .configure_column(index, nucleo::pattern::Variant::Optional)
136 }
137
138 self.column_options[index] = options
139 }
140
141 #[cfg(feature = "experimental")]
142 pub fn reverse_items(&mut self, reverse_items: bool) {
143 self.nucleo.reverse_items(reverse_items);
144 }
145
146 pub fn injector(&self) -> WorkerInjector<T> {
147 WorkerInjector {
148 inner: self.nucleo.injector(),
149 columns: self.columns.clone(),
150 version: self.version.load(atomic::Ordering::Relaxed),
151 picker_version: self.version.clone(),
152 }
153 }
154
155 pub fn find(&mut self, line: &str) {
156 let old_query = self.query.parse(line);
157 if self.query == old_query {
158 return;
159 }
160 for (i, column) in self
161 .columns
162 .iter()
163 .filter(|column| column.filter)
164 .enumerate()
165 {
166 let pattern = self
167 .query
168 .get(&column.name)
169 .map(|s| &**s)
170 .unwrap_or_else(|| {
171 self.column_options[i]
172 .contains(ColumnOptions::OrUseDefault)
173 .then(|| self.query.primary_column_query())
174 .flatten()
175 .unwrap_or_default()
176 });
177
178 let old_pattern = old_query
179 .get(&column.name)
180 .map(|s| &**s)
181 .unwrap_or_else(|| {
182 self.column_options[i]
183 .contains(ColumnOptions::OrUseDefault)
184 .then(|| {
185 let name = self.query.primary_column_name()?;
186 old_query.get(name).map(|s| &**s)
187 })
188 .flatten()
189 .unwrap_or_default()
190 });
191
192 if pattern == old_pattern {
194 continue;
195 }
196 let is_append = pattern.starts_with(old_pattern);
197
198 self.nucleo.pattern.reparse(
199 i,
200 pattern,
201 nucleo::pattern::CaseMatching::Smart,
202 nucleo::pattern::Normalization::Smart,
203 is_append,
204 );
205 }
206 }
207
208 pub fn get_nth(&self, n: u32) -> Option<&T> {
210 self.nucleo
211 .snapshot()
212 .get_matched_item(n)
213 .map(|item| item.data)
214 }
215
216 pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
217 let nucleo::Status { changed, running } = nucleo.tick(10);
218 let snapshot = nucleo.snapshot();
219 (
220 snapshot,
221 Status {
222 item_count: snapshot.item_count(),
223 matched_count: snapshot.matched_item_count(),
224 running,
225 changed,
226 },
227 )
228 }
229
230 pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
231 let snapshot = self.nucleo.snapshot();
232 snapshot.matched_items(..).map(|item| item.data)
233 }
234
235 pub fn counts(&self) -> (u32, u32) {
237 let snapshot = self.nucleo.snapshot();
238 (snapshot.matched_item_count(), snapshot.item_count())
239 }
240
241 #[cfg(feature = "experimental")]
242 pub fn set_stability(&mut self, threshold: u32) {
243 self.nucleo.set_stability(threshold);
244 }
245
246 #[cfg(feature = "experimental")]
247 pub fn get_stability(&self) -> u32 {
248 self.nucleo.get_stability()
249 }
250
251 pub fn restart(&mut self, clear_snapshot: bool) {
252 self.nucleo.restart(clear_snapshot);
253 }
254}
255
256#[derive(Debug, Default, Clone)]
257pub struct Status {
258 pub item_count: u32,
259 pub matched_count: u32,
260 pub running: bool,
261 pub changed: bool,
262}
263
264#[derive(Debug, thiserror::Error)]
265pub enum WorkerError {
266 #[error("the matcher injector has been shut down")]
267 InjectorShutdown,
268 #[error("{0}")]
269 Custom(&'static str),
270}
271
272pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
274
275impl<T: SSS> Worker<T> {
276 pub fn results(
284 &mut self,
285 start: u32,
286 end: u32,
287 width_limits: &[u16],
288 wrap: bool,
289 highlight_style: Style,
290 matcher: &mut nucleo::Matcher,
291 autoscroll: AutoscrollSettings,
292 hscroll_offset: i8,
293 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
294 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
295
296 let mut widths = vec![0u16; self.columns.len()];
297
298 let iter =
299 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
300
301 let table = iter
302 .map(|item| {
303 let mut widths = widths.iter_mut();
304
305 let row = self
306 .columns
307 .iter()
308 .enumerate()
309 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
310 .map(|((col_idx, column), &width_limit)| {
311 let max_width = widths.next().unwrap();
312 let cell = column.format(item.data);
313
314 let (cell, width) = if width_limit == 0 {
315 (Default::default(), if wrap { 1 } else { cell.width() })
316 } else if column.filter {
317 render_cell(
318 cell,
319 col_idx,
320 snapshot,
321 &item,
322 matcher,
323 highlight_style,
324 wrap,
325 width_limit,
326 &mut self.col_indices_buffer,
327 autoscroll,
328 hscroll_offset,
329 )
330 } else if wrap {
332 let (cell, wrapped) = wrap_text(cell, width_limit.saturating_sub(1));
333
334 let width = if wrapped {
335 width_limit as usize
336 } else {
337 cell.width()
338 };
339 (cell, width)
340 } else {
341 let width = cell.width();
342 (cell, width)
343 };
344
345 if width as u16 > *max_width {
347 *max_width = width as u16;
348 }
349
350 cell
351 });
352
353 (row.collect(), item.data)
354 })
355 .collect();
356
357 for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
359 let name_width = c.name.width() as u16;
360 if *w != 0 {
361 *w = (*w).max(name_width);
362 }
363 }
364
365 (table, widths, status)
366 }
367
368 pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
369 let (i, col) = self
370 .columns
371 .iter()
372 .enumerate()
373 .find(|(_, c)| column == &*c.name)?;
374
375 let query = self.query.get(column).map(|s| &**s).or_else(|| {
376 self.column_options[i]
377 .contains(ColumnOptions::OrUseDefault)
378 .then(|| self.query.primary_column_query())
379 .flatten()
380 })?;
381
382 let snapshot = self.nucleo.snapshot();
383 snapshot.matched_items(..).find_map(|item| {
384 let content = col.format_text(item.data);
385 if content.as_str() == query {
386 Some(item.data)
387 } else {
388 None
389 }
390 })
391 }
392
393 pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
394 self.columns
395 .iter()
396 .find(|c| &*c.name == col)
397 .map(|c| c.format_text(item))
398 }
399}
400
401fn render_cell<T: SSS>(
402 cell: Text<'_>,
403 col_idx: usize,
404 snapshot: &nucleo::Snapshot<T>,
405 item: &nucleo::Item<T>,
406 matcher: &mut nucleo::Matcher,
407 highlight_style: Style,
408 wrap: bool,
409 width_limit: u16,
410 col_indices_buffer: &mut Vec<u32>,
411 mut autoscroll: AutoscrollSettings,
412 hscroll_offset: i8,
413) -> (Text<'static>, usize) {
414 autoscroll.end &= !wrap;
416
417 let mut cell_width = 0;
418 let mut wrapped = false;
419
420 let indices_buffer = col_indices_buffer;
422 indices_buffer.clear();
423 snapshot.pattern().column_pattern(col_idx).indices(
424 item.matcher_columns[col_idx].slice(..),
425 matcher,
426 indices_buffer,
427 );
428 indices_buffer.sort_unstable();
429 indices_buffer.dedup();
430 let mut indices = indices_buffer.drain(..);
431
432 let mut lines = vec![];
433 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
434 let mut grapheme_idx = 0u32;
435
436 let mut line_graphemes = Vec::new();
437
438 for line in &cell {
439 line_graphemes.clear();
441 let mut match_idx = None;
442
443 for span in line {
444 for grapheme in span.content.graphemes(true) {
450 let is_match = grapheme_idx == next_highlight_idx;
451
452 let style = if is_match {
453 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
454 span.style.patch(highlight_style)
455 } else {
456 span.style
457 };
458
459 if is_match && (autoscroll.end || match_idx.is_none()) {
460 match_idx = Some(line_graphemes.len());
461 }
462
463 line_graphemes.push((grapheme, style));
464 grapheme_idx += 1;
465 }
466 }
467
468 let mut i; if autoscroll.enabled && autoscroll.end {
472 i = match_idx.unwrap_or(line_graphemes.len());
473
474 let target_width = if let Some(x) = match_idx {
475 (width_limit as usize)
476 .saturating_sub(autoscroll.context.min(line_graphemes.len() - x - 1))
477 } else {
478 width_limit as usize
479 }
480 .saturating_sub(1);
481
482 let mut current_width = 0;
483
484 while i > 0 {
485 let w = line_graphemes[i - 1].0.width();
486 if current_width + w > target_width {
487 break;
488 }
489 i -= 1;
490 current_width += w;
491 }
492 if i > 1 {
493 i += 1;
494 } else {
495 i = 0;
496 }
497 } else if autoscroll.enabled
498 && let Some(m_idx) = match_idx
499 {
500 i = (m_idx as i32 + hscroll_offset as i32 - autoscroll.context as i32).max(0) as usize;
501
502 let mut tail_width: usize = line_graphemes[i..].iter().map(|(g, _)| g.width()).sum();
503
504 let preserved_width = line_graphemes
505 [..autoscroll.initial_preserved.min(line_graphemes.len())]
506 .iter()
507 .map(|(g, _)| g.width())
508 .sum::<usize>();
509
510 while i > autoscroll.initial_preserved {
512 let prev_width = line_graphemes[i - 1].0.width();
513 if tail_width + preserved_width + 1 + prev_width <= width_limit as usize {
514 i -= 1;
515 tail_width += prev_width;
516 } else {
517 break;
518 }
519 }
520
521 if i <= autoscroll.initial_preserved + 1 {
522 i = 0;
523 }
524 } else {
525 i = hscroll_offset.max(0) as usize;
526 };
527
528 let mut current_spans = Vec::new();
530 let mut current_span = String::new();
531 let mut current_style = Style::default();
532 let mut current_width = 0;
533
534 if i > 0 && autoscroll.enabled {
536 let preserved = autoscroll.initial_preserved;
537 for (g, s) in line_graphemes.drain(..preserved) {
538 if s != current_style {
539 if !current_span.is_empty() {
540 current_spans.push(Span::styled(current_span, current_style));
541 }
542 current_span = String::new();
543 current_style = s;
544 }
545 current_span.push_str(g);
546 }
547 if !current_span.is_empty() {
548 current_spans.push(Span::styled(current_span, current_style));
549 }
550 i -= preserved;
551
552 current_width += current_spans.iter().map(|x| x.width()).sum::<usize>();
553 current_spans.push(hscroll_indicator());
554 current_width += 1;
555
556 current_span = String::new();
557 current_style = Style::default();
558 }
559
560 let full_line_width = (!wrap).then(|| {
561 current_width
562 + line_graphemes[i..]
563 .iter()
564 .map(|(g, _)| g.width())
565 .sum::<usize>()
566 });
567
568 let mut graphemes = line_graphemes.drain(i..);
569
570 while let Some((mut grapheme, mut style)) = graphemes.next() {
571 if current_width + grapheme.width() > width_limit as usize {
572 if !current_span.is_empty() {
573 current_spans.push(Span::styled(current_span, current_style));
574 current_span = String::new();
575 }
576 if wrap {
577 current_spans.push(wrapping_indicator());
578 lines.push(Line::from(take(&mut current_spans)));
579
580 current_width = 0;
581 wrapped = true;
582 } else {
583 break;
584 }
585 } else if current_width + grapheme.width() == width_limit as usize {
586 if wrap {
587 let mut new = grapheme.to_string();
588 if current_style != style {
589 current_spans.push(Span::styled(take(&mut current_span), current_style));
590 current_style = style;
591 };
592 while let Some((grapheme2, style2)) = graphemes.next() {
593 if grapheme2.width() == 0 {
594 new.push_str(grapheme2);
595 } else {
596 if !current_span.is_empty() {
597 current_spans.push(Span::styled(current_span, current_style));
598 }
599 current_spans.push(wrapping_indicator());
600 lines.push(Line::from(take(&mut current_spans)));
601
602 current_span = new.clone(); current_width = grapheme.width();
605 wrapped = true;
606
607 grapheme = grapheme2;
608 style = style2;
609 break; }
611 }
612 if !wrapped {
613 current_span.push_str(&new);
614 current_spans.push(Span::styled(take(&mut current_span), style));
616 current_style = style;
617 current_width += grapheme.width();
618 break;
619 }
620 } else {
621 if style != current_style {
622 if !current_span.is_empty() {
623 current_spans.push(Span::styled(current_span, current_style));
624 }
625 current_span = String::new();
626 current_style = style;
627 }
628 current_span.push_str(grapheme);
629 current_width += grapheme.width();
630 break;
631 }
632 }
633
634 if style != current_style {
636 if !current_span.is_empty() {
637 current_spans.push(Span::styled(current_span, current_style))
638 }
639 current_span = String::new();
640 current_style = style;
641 }
642 current_span.push_str(grapheme);
643 current_width += grapheme.width();
644 }
645
646 current_spans.push(Span::styled(current_span, current_style));
647 lines.push(Line::from(current_spans));
648 cell_width = cell_width.max(full_line_width.unwrap_or(current_width));
649
650 grapheme_idx += 1; }
652
653 (
654 Text::from(lines),
655 if wrapped {
656 width_limit as usize
657 } else {
658 cell_width
659 },
660 )
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use nucleo::{Matcher, Nucleo};
667 use ratatui::style::{Color, Style};
668 use ratatui::text::Text;
669 use std::sync::Arc;
670
671 fn setup_nucleo_mocks(
673 search_query: &str,
674 item_text: &str,
675 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
676 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
677
678 let injector = nucleo.injector();
679 injector.push(item_text.to_string(), |item, columns| {
680 columns[0] = item.clone().into();
681 });
682
683 nucleo.pattern.reparse(
684 0,
685 search_query,
686 nucleo::pattern::CaseMatching::Ignore,
687 nucleo::pattern::Normalization::Smart,
688 false,
689 );
690
691 nucleo.tick(10); let matcher = Matcher::default();
694 let buffer = Vec::new();
695
696 (nucleo, matcher, buffer)
697 }
698
699 #[test]
700 fn test_no_scroll_context_renders_normally() {
701 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
702 let snapshot = nucleo.snapshot();
703 let item = snapshot.get_item(0).unwrap();
704
705 let cell = Text::from("hello match world");
706 let highlight = Style::default().fg(Color::Red);
707
708 let (result_text, width) = render_cell(
709 cell,
710 0,
711 &snapshot,
712 &item,
713 &mut matcher,
714 highlight,
715 false,
716 u16::MAX,
717 &mut buffer,
718 AutoscrollSettings {
719 enabled: false,
720 ..Default::default()
721 },
722 0,
723 );
724
725 let output_str = text_to_string(&result_text);
726 assert_eq!(output_str, "hello match world");
727 assert_eq!(width, 17);
728 }
729
730 #[test]
731 fn test_scroll_context_cuts_prefix_correctly() {
732 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
733 let snapshot = nucleo.snapshot();
734 let item = snapshot.get_item(0).unwrap();
735
736 let cell = Text::from("hello match world");
737 let highlight = Style::default().fg(Color::Red);
738
739 let (result_text, _) = render_cell(
740 cell,
741 0,
742 &snapshot,
743 &item,
744 &mut matcher,
745 highlight,
746 false,
747 u16::MAX,
748 &mut buffer,
749 AutoscrollSettings {
750 initial_preserved: 0,
751 context: 2,
752 ..Default::default()
753 },
754 0,
755 );
756
757 let output_str = text_to_string(&result_text);
758 assert_eq!(output_str, "hello match world");
759 }
760
761 #[test]
762 fn test_scroll_context_backfills_to_fill_width_limit() {
763 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
778 let snapshot = nucleo.snapshot();
779 let item = snapshot.get_item(0).unwrap();
780
781 let cell = Text::from("abcdefghijmatch");
782 let highlight = Style::default().fg(Color::Red);
783
784 let (result_text, width) = render_cell(
785 cell,
786 0,
787 &snapshot,
788 &item,
789 &mut matcher,
790 highlight,
791 false,
792 10,
793 &mut buffer,
794 AutoscrollSettings {
795 initial_preserved: 0,
796 context: 1,
797 ..Default::default()
798 },
799 0,
800 );
801
802 let output_str = text_to_string(&result_text);
803 assert_eq!(output_str, "…ghijmatch");
804 assert_eq!(width, 10);
805 }
806
807 #[test]
808 fn test_preserved_prefix_and_ellipsis() {
809 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
822 let snapshot = nucleo.snapshot();
823 let item = snapshot.get_item(0).unwrap();
824
825 let cell = Text::from("abcdefghijmatch");
826 let highlight = Style::default().fg(Color::Red);
827
828 let (result_text, width) = render_cell(
829 cell,
830 0,
831 &snapshot,
832 &item,
833 &mut matcher,
834 highlight,
835 false,
836 10,
837 &mut buffer,
838 AutoscrollSettings {
839 initial_preserved: 3,
840 context: 1,
841 ..Default::default()
842 },
843 0,
844 );
845
846 let output_str = text_to_string(&result_text);
847 assert_eq!(output_str, "abc…jmatch");
848 assert_eq!(width, 10);
849 }
850
851 #[test]
852 fn test_wrap() {
853 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
854 let snapshot = nucleo.snapshot();
855 let item = snapshot.get_item(0).unwrap();
856
857 let cell = Text::from("abcdefmatch");
858 let highlight = Style::default().fg(Color::Red);
859
860 let (result_text, width) = render_cell(
861 cell,
862 0,
863 &snapshot,
864 &item,
865 &mut matcher,
866 highlight,
867 true,
868 10,
869 &mut buffer,
870 AutoscrollSettings {
871 initial_preserved: 3,
872 context: 1,
873 ..Default::default()
874 },
875 -2,
876 );
877
878 let output_str = text_to_string(&result_text);
879 assert_eq!(output_str, "abcdefmat↵\nch");
880 assert_eq!(width, 10);
881 }
882
883 #[test]
884 fn test_wrap_edge_case_6_chars_width_5() {
885 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("", "123456");
886 let snapshot = nucleo.snapshot();
887 let item = snapshot.get_item(0).unwrap();
888
889 let cell = Text::from("123456");
890 let highlight = Style::default().fg(Color::Red);
891
892 let (result_text, width) = render_cell(
893 cell,
894 0,
895 &snapshot,
896 &item,
897 &mut matcher,
898 highlight,
899 true,
900 5,
901 &mut buffer,
902 AutoscrollSettings {
903 enabled: false,
904 ..Default::default()
905 },
906 0,
907 );
908
909 let output_str = text_to_string(&result_text);
910 assert_eq!(output_str, "1234↵\n56");
912 assert_eq!(width, 5);
913 }
914
915 #[test]
916 fn test_autoscroll_end() {
917 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
918 let snapshot = nucleo.snapshot();
919 let item = snapshot.get_item(0).unwrap();
920
921 let cell = Text::from("abcdefghijmatch");
922 let highlight = Style::default().fg(Color::Red);
923
924 let (result_text, width) = render_cell(
925 cell,
926 0,
927 &snapshot,
928 &item,
929 &mut matcher,
930 highlight,
931 false,
932 10,
933 &mut buffer,
934 AutoscrollSettings {
935 end: true,
936 context: 4,
937 ..Default::default()
938 },
939 0,
940 );
941
942 let output_str = text_to_string(&result_text);
943 assert_eq!(output_str, "…ghijmatch");
944 assert_eq!(width, 10);
945 }
946}