1use std::sync::{Arc, Mutex, PoisonError};
73
74use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Table, Text};
75
76use super::data::normalize;
77use super::stream::table_lines;
78use super::{Column, Frame, Value, frame_builders, style};
79
80pub const MAX_HEIGHT: usize = 10_000;
83
84pub const MAX_SAMPLE: usize = 10_000;
86
87pub const DEFAULT_SAMPLE: usize = 100;
89
90pub const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
92
93pub trait VirtualRows {
105 fn row_count(&self) -> Option<usize> {
107 None
108 }
109
110 fn row(&self, index: usize) -> Option<Vec<Value>>;
112
113 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
116 (start..start.saturating_add(len))
117 .map_while(|index| self.row(index))
118 .collect()
119 }
120
121 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
129 self.rows(start, len)
130 .iter()
131 .map(|row| pick(row, columns))
132 .collect()
133 }
134}
135
136fn pick(row: &[Value], columns: &[usize]) -> Vec<Value> {
138 columns
139 .iter()
140 .map(|&c| row.get(c).cloned().unwrap_or(Value::Null))
141 .collect()
142}
143
144impl VirtualRows for [Vec<Value>] {
145 fn row_count(&self) -> Option<usize> {
146 Some(self.len())
147 }
148
149 fn row(&self, index: usize) -> Option<Vec<Value>> {
150 self.get(index).cloned()
151 }
152
153 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
154 let start = start.min(self.len());
155 let end = start.saturating_add(len).min(self.len());
156 self[start..end].to_vec()
157 }
158
159 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
160 let start = start.min(self.len());
161 let end = start.saturating_add(len).min(self.len());
162 self[start..end]
163 .iter()
164 .map(|row| pick(row, columns))
165 .collect()
166 }
167}
168
169impl VirtualRows for Vec<Vec<Value>> {
170 fn row_count(&self) -> Option<usize> {
171 Some(self.len())
172 }
173
174 fn row(&self, index: usize) -> Option<Vec<Value>> {
175 self.as_slice().row(index)
176 }
177
178 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
179 self.as_slice().rows(start, len)
180 }
181
182 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
183 self.as_slice().rows_columns(start, len, columns)
184 }
185}
186
187macro_rules! forward_rows {
188 ($($ptr:ty),*) => {$(
189 impl<T: VirtualRows + ?Sized> VirtualRows for $ptr {
190 fn row_count(&self) -> Option<usize> {
191 (**self).row_count()
192 }
193
194 fn row(&self, index: usize) -> Option<Vec<Value>> {
195 (**self).row(index)
196 }
197
198 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
199 (**self).rows(start, len)
200 }
201
202 fn rows_columns(
203 &self,
204 start: usize,
205 len: usize,
206 columns: &[usize],
207 ) -> Vec<Vec<Value>> {
208 (**self).rows_columns(start, len, columns)
209 }
210 }
211 )*};
212}
213forward_rows!(&T, Box<T>, Arc<T>);
214
215pub struct FnCells<F> {
231 rows: usize,
232 columns: usize,
233 cell: F,
234}
235
236impl<F> FnCells<F>
237where
238 F: Fn(usize, usize) -> Value,
239{
240 pub fn new(rows: usize, columns: usize, cell: F) -> Self {
242 FnCells {
243 rows,
244 columns,
245 cell,
246 }
247 }
248}
249
250impl<F> std::fmt::Debug for FnCells<F> {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("FnCells")
253 .field("rows", &self.rows)
254 .field("columns", &self.columns)
255 .finish_non_exhaustive()
256 }
257}
258
259impl<F> VirtualRows for FnCells<F>
260where
261 F: Fn(usize, usize) -> Value,
262{
263 fn row_count(&self) -> Option<usize> {
264 Some(self.rows)
265 }
266
267 fn row(&self, index: usize) -> Option<Vec<Value>> {
268 (index < self.rows).then(|| (0..self.columns).map(|c| (self.cell)(index, c)).collect())
269 }
270
271 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
272 let end = start.saturating_add(len).min(self.rows);
273 (start.min(end)..end)
274 .map(|r| {
275 columns
276 .iter()
277 .map(|&c| {
278 if c < self.columns {
279 (self.cell)(r, c)
280 } else {
281 Value::Null
282 }
283 })
284 .collect()
285 })
286 .collect()
287 }
288}
289
290pub struct FnRows<F> {
302 count: Option<usize>,
303 row: F,
304}
305
306impl<F> FnRows<F>
307where
308 F: Fn(usize) -> Option<Vec<Value>>,
309{
310 pub fn new(count: Option<usize>, row: F) -> Self {
312 FnRows { count, row }
313 }
314}
315
316impl<F> std::fmt::Debug for FnRows<F> {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("FnRows")
319 .field("count", &self.count)
320 .finish_non_exhaustive()
321 }
322}
323
324impl<F> VirtualRows for FnRows<F>
325where
326 F: Fn(usize) -> Option<Vec<Value>>,
327{
328 fn row_count(&self) -> Option<usize> {
329 self.count
330 }
331
332 fn row(&self, index: usize) -> Option<Vec<Value>> {
333 if self.count.is_some_and(|count| index >= count) {
334 return None;
335 }
336 (self.row)(index)
337 }
338}
339
340#[derive(Clone, Debug, PartialEq)]
342#[non_exhaustive]
343pub struct Page {
344 pub start: usize,
348 pub rows: Vec<Vec<Value>>,
351 pub total: Option<usize>,
354 pub more: bool,
356 pub columns: Vec<usize>,
359 pub frozen: usize,
361 pub column_total: usize,
363}
364
365impl Page {
366 pub fn range(&self) -> std::ops::Range<usize> {
368 self.start..self.start + self.rows.len()
369 }
370
371 pub fn column_range(&self) -> std::ops::Range<usize> {
373 match self.columns.get(self.frozen..) {
374 Some([first, .., last]) => *first..*last + 1,
375 Some([only]) => *only..*only + 1,
376 _ => 0..0,
377 }
378 }
379
380 pub fn column_position(&self, ascii: bool) -> Option<String> {
385 if self.columns.len() >= self.column_total {
386 return None;
387 }
388 let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
389 let range = self.column_range();
390 let total = number(self.column_total);
391 Some(match range.len() {
392 0 => format!("no columns of {total}"),
393 1 => format!("column {} of {total}", number(range.start + 1)),
394 _ => {
395 let dash = if ascii { "-" } else { "–" };
396 format!(
397 "columns {}{dash}{} of {total}",
398 number(range.start + 1),
399 number(range.end)
400 )
401 }
402 })
403 }
404
405 pub fn position(&self, ascii: bool) -> String {
410 let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
411 if self.rows.is_empty() {
412 return match self.start {
413 0 => "no rows".to_string(),
414 start => format!("no rows from {}", number(start + 1)),
415 };
416 }
417 let dash = if ascii { "-" } else { "–" };
418 let range = format!(
419 "rows {}{dash}{}",
420 number(self.start + 1),
421 number(self.start + self.rows.len())
422 );
423 match self.total {
424 Some(total) => format!("{range} of {}", number(total)),
425 None if self.more => {
426 format!(
427 "{range} of {}+",
428 number(self.start.saturating_add(self.rows.len() + 1))
429 )
430 }
431 None => range,
432 }
433 }
434}
435
436#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
442pub struct VirtualTable<S> {
443 source: S,
444 columns: Vec<Column>,
445 frame: Frame,
446 offset: usize,
447 height: usize,
448 widths: Vec<Option<usize>>,
449 sample: usize,
450 max_width: usize,
451 wrap: bool,
452 position: bool,
453 row_numbers: bool,
454 fit_window: bool,
455 null: Option<String>,
456 footnote: Option<String>,
457 sampled: Mutex<Vec<Option<usize>>>,
459 window: ColumnWindow,
460}
461
462#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
465struct ColumnWindow {
466 offset: usize,
467 count: Option<usize>,
468 fit: bool,
469 frozen: usize,
470}
471
472impl ColumnWindow {
473 fn on(&self) -> bool {
475 self.offset > 0 || self.count.is_some() || self.fit || self.frozen > 0
476 }
477}
478
479impl<S> std::fmt::Debug for VirtualTable<S> {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 f.debug_struct("VirtualTable")
482 .field("columns", &self.columns)
483 .field("offset", &self.offset)
484 .field("height", &self.height)
485 .field("widths", &self.widths)
486 .field("sample", &self.sample)
487 .field("window", &self.window)
488 .finish_non_exhaustive()
489 }
490}
491
492frame_builders!([S] VirtualTable<S>);
493
494impl<S: VirtualRows> VirtualTable<S> {
495 pub fn new(columns: impl IntoIterator<Item = Column>, source: S) -> Self {
497 VirtualTable {
498 source,
499 columns: columns.into_iter().collect(),
500 frame: Frame::default(),
501 offset: 0,
502 height: 20,
503 widths: Vec::new(),
504 sample: DEFAULT_SAMPLE,
505 max_width: DEFAULT_MAX_COLUMN_WIDTH,
506 wrap: false,
507 position: true,
508 row_numbers: false,
509 fit_window: false,
510 null: None,
511 footnote: None,
512 sampled: Mutex::new(Vec::new()),
513 window: ColumnWindow::default(),
514 }
515 }
516
517 pub fn offset(mut self, offset: usize) -> Self {
519 self.offset = offset;
520 self
521 }
522
523 pub fn height(mut self, rows: usize) -> Self {
525 self.set_height(rows);
526 self
527 }
528
529 pub fn widths(mut self, widths: impl IntoIterator<Item = impl Into<Option<usize>>>) -> Self {
533 self.widths = widths.into_iter().map(Into::into).collect();
534 self.resample();
535 self
536 }
537
538 pub fn sample(mut self, rows: usize) -> Self {
542 self.sample = rows.min(MAX_SAMPLE);
543 self.resample();
544 self
545 }
546
547 pub fn max_column_width(mut self, cells: usize) -> Self {
550 self.max_width = cells.max(1);
551 self.resample();
552 self
553 }
554
555 pub fn wrap(mut self, wrap: bool) -> Self {
558 self.wrap = wrap;
559 self
560 }
561
562 pub fn show_position(mut self, show: bool) -> Self {
564 self.position = show;
565 self
566 }
567
568 pub fn fit_window(mut self, fit: bool) -> Self {
572 self.fit_window = fit;
573 self
574 }
575
576 pub fn row_numbers(mut self, show: bool) -> Self {
578 self.row_numbers = show;
579 self
580 }
581
582 pub fn null_marker(mut self, marker: impl Into<String>) -> Self {
586 self.null = Some(marker.into());
587 self.resample();
588 self
589 }
590
591 pub fn footnote(mut self, note: impl Into<String>) -> Self {
594 self.footnote = Some(note.into());
595 self
596 }
597
598 pub fn column_offset(mut self, offset: usize) -> Self {
603 self.window.offset = offset;
604 self
605 }
606
607 pub fn visible_columns(mut self, count: usize) -> Self {
611 self.window.count = Some(count.max(1));
612 self
613 }
614
615 pub fn fit_columns(mut self, fit: bool) -> Self {
621 self.window.fit = fit;
622 self
623 }
624
625 pub fn frozen_columns(mut self, count: usize) -> Self {
629 self.window.frozen = count;
630 self
631 }
632
633 pub fn set_column_offset(&mut self, offset: usize) {
635 self.window.offset = offset;
636 }
637
638 pub fn set_visible_columns(&mut self, count: Option<usize>) {
641 self.window.count = count.map(|c| c.max(1));
642 }
643
644 pub fn scroll_columns_by(&mut self, columns: isize) {
647 let offset = self.window.offset.saturating_add_signed(columns);
648 self.window.offset = offset.min(self.max_column_offset());
649 }
650
651 pub fn window_column_offset(&self) -> usize {
653 self.window.offset
654 }
655
656 pub fn max_column_offset(&self) -> usize {
660 let scrollable = self.columns.len().saturating_sub(self.window.frozen);
661 scrollable.saturating_sub(self.window.count.unwrap_or(1))
662 }
663
664 pub fn shown_columns(&self, width: usize) -> Vec<usize> {
668 let n = self.columns.len();
669 if !self.window.on() {
670 return (0..n).collect();
671 }
672 let frozen = self.window.frozen.min(n);
673 let scrollable = n - frozen;
674 let start = frozen + self.window.offset.min(scrollable.saturating_sub(1));
675 let end = match self.window.count {
676 Some(count) => start.saturating_add(count).min(n),
677 None => n,
678 };
679 let mut shown: Vec<usize> = (0..frozen).collect();
680 if !self.window.fit || start >= end {
681 shown.extend(start..end);
682 return shown;
683 }
684 let mut widths = self.widths_of(&shown);
687 if self.row_numbers {
688 let last = self
689 .row_count()
690 .unwrap_or(self.offset.saturating_add(self.height));
691 widths.insert(0, last.max(1).to_string().len());
692 }
693 let mut next = start;
694 'fit: while next < end {
695 let room = width.saturating_sub(self.table_width(&widths));
696 let batch = (room / (self.max_width + 3)).max(1);
697 let candidates: Vec<usize> = (next..end.min(next + batch)).collect();
698 for (index, w) in candidates.iter().zip(self.widths_of(&candidates)) {
699 widths.push(w);
700 if shown.len() > frozen && self.table_width(&widths) > width {
701 break 'fit;
702 }
703 shown.push(*index);
704 }
705 next += candidates.len();
706 }
707 shown
708 }
709
710 fn table_width(&self, widths: &[usize]) -> usize {
713 let boxed = self.frame.box_set.is_some();
714 let edges = if boxed && self.frame.show_edge { 2 } else { 0 };
715 let dividers = if boxed {
716 widths.len().saturating_sub(1)
717 } else {
718 0
719 };
720 widths.iter().map(|w| w + 2).sum::<usize>() + dividers + edges
721 }
722
723 pub fn set_offset(&mut self, offset: usize) {
725 self.offset = offset;
726 }
727
728 pub fn set_height(&mut self, rows: usize) {
730 self.height = rows.min(MAX_HEIGHT);
731 }
732
733 pub fn scroll_by(&mut self, rows: isize) {
736 let offset = self.offset.saturating_add_signed(rows);
737 self.offset = match self.max_offset() {
738 Some(max) => offset.min(max),
739 None => offset,
740 };
741 }
742
743 pub fn window_offset(&self) -> usize {
745 self.offset
746 }
747
748 pub fn window_height(&self) -> usize {
750 self.height
751 }
752
753 pub fn row_count(&self) -> Option<usize> {
755 self.source.row_count()
756 }
757
758 pub fn max_offset(&self) -> Option<usize> {
760 self.row_count()
761 .map(|count| count.saturating_sub(self.height))
762 }
763
764 pub fn columns(&self) -> &[Column] {
766 &self.columns
767 }
768
769 pub fn source(&self) -> &S {
771 &self.source
772 }
773
774 pub fn source_mut(&mut self) -> &mut S {
777 &mut self.source
778 }
779
780 pub fn resample(&self) {
782 self.sampled
783 .lock()
784 .unwrap_or_else(PoisonError::into_inner)
785 .clear();
786 }
787
788 pub fn page(&self) -> Page {
794 self.page_at(80)
795 }
796
797 pub fn page_at(&self, width: usize) -> Page {
801 let columns = self.shown_columns(width);
802 let count = self.source.row_count();
803 let start = match count {
804 Some(count) => self.offset.min(count.saturating_sub(self.height)),
805 None => self.offset,
806 };
807 let fetch = if count.is_some() {
809 self.height
810 } else {
811 self.height + 1
812 };
813 let mut rows = if self.window.on() {
814 self.source.rows_columns(start, fetch, &columns)
815 } else {
816 self.source.rows(start, fetch)
817 };
818 rows.truncate(fetch);
819 let more = match count {
820 Some(count) => start + rows.len().min(self.height) < count,
821 None => rows.len() > self.height,
822 };
823 rows.truncate(self.height);
824 let width = columns.len();
825 let rows: Vec<Vec<Value>> = rows.into_iter().map(|row| normalize(row, width)).collect();
826 let total = count.or_else(|| (!more && !rows.is_empty()).then(|| start + rows.len()));
827 Page {
828 start,
829 rows,
830 total,
831 more,
832 frozen: if self.window.on() {
833 self.window.frozen.min(self.columns.len())
834 } else {
835 0
836 },
837 column_total: self.columns.len(),
838 columns,
839 }
840 }
841
842 pub fn column_widths(&self) -> Vec<usize> {
846 let all: Vec<usize> = (0..self.columns.len()).collect();
847 self.widths_of(&all)
848 }
849
850 fn fixed_width(&self, index: usize) -> Option<usize> {
852 self.widths
853 .get(index)
854 .copied()
855 .flatten()
856 .or(self.columns[index].column_options().width)
857 }
858
859 fn widths_of(&self, indices: &[usize]) -> Vec<usize> {
862 let mut sampled = self.sampled.lock().unwrap_or_else(PoisonError::into_inner);
863 if sampled.len() != self.columns.len() {
864 *sampled = vec![None; self.columns.len()];
865 }
866 let missing: Vec<usize> = indices
867 .iter()
868 .copied()
869 .filter(|&i| sampled[i].is_none() && self.fixed_width(i).is_none())
870 .collect();
871 if !missing.is_empty() {
872 let mut widths: Vec<usize> = missing
873 .iter()
874 .map(|&i| Text::new(self.columns[i].header()).measurement().1)
875 .collect();
876 let rows = if missing.len() == self.columns.len() {
877 self.source.rows(0, self.sample)
879 } else {
880 self.source.rows_columns(0, self.sample, &missing)
881 };
882 for row in rows.into_iter().take(self.sample) {
883 let row = normalize(row, missing.len());
886 for ((width, &index), value) in widths.iter_mut().zip(&missing).zip(&row) {
887 let cell = self.cell(&self.columns[index], value).measurement().1;
888 *width = (*width).max(cell);
889 }
890 }
891 for (width, &index) in widths.into_iter().zip(&missing) {
892 sampled[index] = Some(width.clamp(1, self.max_width));
893 }
894 }
895 indices
896 .iter()
897 .map(|&i| self.fixed_width(i).or(sampled[i]).unwrap_or(self.max_width))
898 .collect()
899 }
900
901 pub fn chrome_lines(&self, console: &Console, width: usize) -> usize {
906 let options = console.options().update_width(width.max(1));
907 let columns = self.shown_columns(width);
908 let empty = Page {
909 start: 0,
910 rows: Vec::new(),
911 total: None,
912 more: false,
913 frozen: 0,
914 column_total: self.columns.len(),
915 columns,
916 };
917 let table = self.build(console, &empty);
918 table_lines(&table, console, &options).len()
919 + usize::from(self.position)
920 + usize::from(self.footnote.is_some())
921 }
922
923 pub fn to_table(&self, console: &Console) -> Table {
926 self.build(console, &self.page_at(console.width()))
927 }
928
929 fn cell(&self, column: &Column, value: &Value) -> Text {
930 match (&self.null, value) {
931 (Some(marker), Value::Null) => Text::new(marker.clone()),
932 _ => column.cell(value),
933 }
934 }
935
936 fn build(&self, console: &Console, page: &Page) -> Table {
937 let mut widths = self.widths_of(&page.columns);
938 if self.fit_window {
939 for (k, (&index, width)) in page.columns.iter().zip(widths.iter_mut()).enumerate() {
940 let column = &self.columns[index];
941 if self.fixed_width(index).is_some() {
942 continue;
943 }
944 for row in &page.rows {
945 let cell = self.cell(column, &row[k]).measurement().1;
946 *width = (*width).max(cell.min(self.max_width));
947 }
948 }
949 }
950 let mut columns: Vec<Column> = Vec::with_capacity(page.columns.len() + 1);
951 if self.row_numbers {
952 let last = page
953 .total
954 .unwrap_or(page.start + page.rows.len())
955 .max(page.start + page.rows.len());
956 let options = rich::ColumnOptions {
957 justify: Justify::Right,
958 width: Some(last.max(1).to_string().len()),
959 no_wrap: true,
960 style: style(console, "table.row_number"),
961 ..Default::default()
962 };
963 columns.push(Column::new("#").options(options));
964 }
965 for (&index, width) in page.columns.iter().zip(&widths) {
966 let column = &self.columns[index];
967 let mut options = column.column_options().clone();
968 options.width = Some(*width);
969 if !self.wrap {
970 options.no_wrap = true;
971 if options.overflow == Overflow::Fold {
972 options.overflow = Overflow::Ellipsis;
973 }
974 }
975 columns.push(Column::new(column.header()).options(options));
976 }
977 let headers: Vec<Text> = columns.iter().map(|c| Text::new(c.header())).collect();
978 let mut table = self.frame.table(&columns, &headers, true, true);
979 let null_style = style(console, "table.null");
980 for (index, row) in page.rows.iter().enumerate() {
981 let mut cells = Vec::with_capacity(columns.len());
982 if self.row_numbers {
983 cells.push(Text::new((page.start + index + 1).to_string()));
984 }
985 for (&index, value) in page.columns.iter().zip(row) {
986 let mut text = self.cell(&self.columns[index], value);
987 if self.null.is_some() && matches!(value, Value::Null) {
988 let len = text.plain().len();
989 text.stylize(null_style.clone(), 0, len);
990 }
991 cells.push(text);
992 }
993 table.add_row_text(cells);
994 }
995 table
996 }
997
998 fn note_lines(
999 &self,
1000 console: &Console,
1001 options: &ConsoleOptions,
1002 text: String,
1003 ) -> Vec<Vec<Segment>> {
1004 let text = Text::styled(text, style(console, "table.position"))
1005 .no_wrap(true)
1006 .overflow(Overflow::Ellipsis);
1007 let mut options = options.clone();
1008 options.height = None;
1009 console.render_lines(&text, &options, false)
1010 }
1011
1012 fn position_text(page: &Page, ascii: bool) -> String {
1015 let rows = page.position(ascii);
1016 match page.column_position(ascii) {
1017 Some(columns) => format!("{rows}, {columns}"),
1018 None => rows,
1019 }
1020 }
1021
1022 fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
1023 let page = self.page_at(options.max_width);
1024 let table = self.build(console, &page);
1025 let mut lines = table_lines(&table, console, options);
1026 if self.position {
1027 let text = Self::position_text(&page, console.ascii_only());
1028 lines.extend(self.note_lines(console, options, text));
1029 }
1030 if let Some(note) = &self.footnote {
1031 lines.extend(self.note_lines(console, options, note.clone()));
1032 }
1033 lines
1034 }
1035}
1036
1037impl<S: VirtualRows> Renderable for VirtualTable<S> {
1038 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1039 crate::event::flatten(self.render_lines(console, options))
1040 }
1041
1042 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
1045 let page = self.page_at(options.max_width);
1046 let mut measurement = self.build(console, &page).measure(console, options);
1047 let notes = self
1048 .position
1049 .then(|| Self::position_text(&page, console.ascii_only()))
1050 .into_iter()
1051 .chain(self.footnote.clone());
1052 for note in notes {
1053 measurement.maximum = measurement.maximum.max(Text::new(note).cell_len());
1054 }
1055 measurement
1056 }
1057}
1058
1059impl<S: VirtualRows> crate::a11y::AccessibleText for VirtualTable<S> {
1060 fn accessible_text(&self, width: usize) -> String {
1061 let console = Console::builder().width(width.max(1)).build();
1062 self.to_table(&console).accessible_text(width)
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069
1070 fn render(table: &impl Renderable, width: usize) -> String {
1071 Console::builder()
1072 .width(width)
1073 .color_system(None)
1074 .build()
1075 .render_export(table)
1076 }
1077
1078 fn numbered(count: Option<usize>) -> FnRows<impl Fn(usize) -> Option<Vec<Value>>> {
1079 FnRows::new(count, |i| {
1080 Some(vec![Value::from(i), format!("r{i}").into()])
1081 })
1082 }
1083
1084 #[test]
1085 fn an_offset_past_the_end_shows_the_last_window() {
1086 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::from(i)]).collect();
1087 let table = VirtualTable::new([Column::new("n")], rows)
1088 .offset(50)
1089 .height(3);
1090 let page = table.page();
1091 assert_eq!(page.range(), 7..10);
1092 assert_eq!(page.total, Some(10));
1093 assert!(!page.more);
1094 assert_eq!(page.position(false), "rows 8–10 of 10");
1095 }
1096
1097 #[test]
1098 fn an_uncounted_source_reports_what_it_knows() {
1099 let rows = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i)]));
1100 let table = VirtualTable::new([Column::new("n")], rows).height(2);
1101 assert_eq!(table.page().position(false), "rows 1–2 of 3+");
1102 let mut table = table;
1103 table.set_offset(3);
1104 let page = table.page();
1105 assert_eq!((page.total, page.more), (Some(5), false));
1106 assert_eq!(page.position(true), "rows 4-5 of 5");
1107 table.set_offset(9);
1108 assert_eq!(table.page().position(false), "no rows from 10");
1109 }
1110
1111 #[test]
1112 fn ragged_rows_are_padded_and_cut() {
1113 let rows = vec![
1114 vec![Value::from("a")],
1115 vec![Value::from("b"), Value::from(2), Value::from("extra")],
1116 vec![],
1117 ];
1118 let table = VirtualTable::new([Column::new("x"), Column::new("y")], rows);
1119 let page = table.page();
1120 assert!(page.rows.iter().all(|row| row.len() == 2));
1121 let out = render(&table, 30);
1122 assert!(out.contains("│ b │ 2 │"), "{out}");
1123 }
1124
1125 #[test]
1126 fn widths_hold_still_while_scrolling() {
1127 let mut table = VirtualTable::new([Column::new("n"), Column::new("s")], numbered(None))
1128 .sample(10)
1129 .height(2);
1130 let first = render(&table, 40);
1131 table.scroll_by(500);
1132 let later = render(&table, 40);
1133 let width = |out: &str| out.lines().next().unwrap().chars().count();
1134 assert_eq!(width(&first), width(&later));
1135 assert!(later.contains("│ … │ r… │"), "{later}");
1137 assert_eq!(table.column_widths(), [1, 2]);
1138 }
1139
1140 #[test]
1141 fn fixed_widths_win_and_the_cap_bounds_samples() {
1142 let long = vec![vec![Value::from("x".repeat(500)), Value::from("y")]];
1143 let table = VirtualTable::new([Column::new("a"), Column::new("b")], long.clone())
1144 .max_column_width(8);
1145 assert_eq!(table.column_widths(), [8, 1]);
1146 let table =
1147 VirtualTable::new([Column::new("a"), Column::new("b")], long).widths([None, Some(5)]);
1148 assert_eq!(table.column_widths(), [40, 5]);
1149 }
1150
1151 #[test]
1152 fn scrolling_stops_at_both_ends() {
1153 let mut table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(10);
1154 table.scroll_by(-5);
1155 assert_eq!(table.window_offset(), 0);
1156 table.scroll_by(1_000);
1157 assert_eq!(table.window_offset(), 90);
1158 assert_eq!(table.max_offset(), Some(90));
1159 }
1160
1161 #[test]
1162 fn nulls_can_be_marked_apart_from_empty_text() {
1163 let rows = vec![vec![Value::Null], vec![Value::from("")]];
1164 let table = VirtualTable::new([Column::new("v")], rows)
1165 .null_marker("NULL")
1166 .show_position(false);
1167 assert_eq!(
1168 render(&table, 20),
1169 "┏━━━━━━┓\n┃ v ┃\n┡━━━━━━┩\n│ NULL │\n│ │\n└──────┘\n"
1170 );
1171 }
1172
1173 #[test]
1174 fn row_numbers_and_footnote() {
1175 let table = VirtualTable::new([Column::new("s")], numbered(Some(1_000)))
1176 .offset(998)
1177 .height(2)
1178 .row_numbers(true)
1179 .widths([3])
1180 .footnote("(1,000 rows)");
1181 let out = render(&table, 40);
1182 assert!(out.contains("│ 999 │ 998 │"), "{out}");
1183 assert!(
1184 out.ends_with("rows 999–1,000 of 1,000\n(1,000 rows)\n"),
1185 "{out}"
1186 );
1187 }
1188
1189 #[test]
1190 fn chrome_lines_count_everything_but_rows() {
1191 let table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(5);
1192 let console = Console::builder().width(40).build();
1193 let out = render(&table, 40);
1194 assert_eq!(out.lines().count(), 5 + table.chrome_lines(&console, 40));
1195 }
1196
1197 #[test]
1198 fn an_empty_source_renders_a_header_and_no_rows() {
1199 let table = VirtualTable::new([Column::new("n")], Vec::<Vec<Value>>::new());
1200 let out = render(&table, 20);
1201 assert!(out.ends_with("no rows\n"), "{out}");
1202 let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3)));
1203 render(&table, 20);
1204 let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3))).fit_columns(true);
1206 assert_eq!(table.shown_columns(20), Vec::<usize>::new());
1207 render(&table, 20);
1208 }
1209
1210 struct Counting {
1213 rows: usize,
1214 columns: usize,
1215 cells: std::sync::atomic::AtomicUsize,
1216 seen: Mutex<std::collections::BTreeSet<usize>>,
1217 }
1218
1219 impl Counting {
1220 fn new(rows: usize, columns: usize) -> Self {
1221 Counting {
1222 rows,
1223 columns,
1224 cells: Default::default(),
1225 seen: Default::default(),
1226 }
1227 }
1228
1229 fn seen(&self) -> Vec<usize> {
1230 self.seen.lock().unwrap().iter().copied().collect()
1231 }
1232
1233 fn cells(&self) -> usize {
1234 self.cells.load(std::sync::atomic::Ordering::Relaxed)
1235 }
1236 }
1237
1238 impl VirtualRows for Counting {
1239 fn row_count(&self) -> Option<usize> {
1240 Some(self.rows)
1241 }
1242
1243 fn row(&self, index: usize) -> Option<Vec<Value>> {
1244 let all: Vec<usize> = (0..self.columns).collect();
1245 self.rows_columns(index, 1, &all).pop()
1246 }
1247
1248 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
1249 let end = start.saturating_add(len).min(self.rows);
1250 let rows: Vec<Vec<Value>> = (start.min(end)..end)
1251 .map(|r| {
1252 columns
1253 .iter()
1254 .map(|c| Value::from(format!("r{r}c{c}")))
1255 .collect()
1256 })
1257 .collect();
1258 self.cells.fetch_add(
1259 rows.len() * columns.len(),
1260 std::sync::atomic::Ordering::Relaxed,
1261 );
1262 self.seen.lock().unwrap().extend(columns.iter().copied());
1263 rows
1264 }
1265 }
1266
1267 fn twelve() -> Vec<Column> {
1268 (0..12).map(|c| Column::new(format!("c{c}"))).collect()
1269 }
1270
1271 #[test]
1272 fn a_column_window_fetches_only_its_columns() {
1273 let source = Counting::new(5, 12);
1274 let table = VirtualTable::new(twelve(), &source)
1275 .height(2)
1276 .column_offset(3)
1277 .visible_columns(2);
1278 assert_eq!(
1279 render(&table, 60),
1280 "\
1281┏━━━━━━┳━━━━━━┓
1282┃ c3 ┃ c4 ┃
1283┡━━━━━━╇━━━━━━┩
1284│ r0c3 │ r0c4 │
1285│ r1c3 │ r1c4 │
1286└──────┴──────┘
1287rows 1–2 of 5, columns 4–5 of 12
1288"
1289 );
1290 assert_eq!(source.seen(), vec![3, 4]);
1291 assert_eq!(source.cells(), 5 * 2 + 2 * 2);
1293 let page = table.page();
1294 assert_eq!(
1295 (page.columns.clone(), page.frozen, page.column_total),
1296 (vec![3, 4], 0, 12)
1297 );
1298 assert_eq!(page.column_range(), 3..5);
1299 }
1300
1301 #[test]
1302 fn frozen_columns_stay_on_the_left() {
1303 let source = Counting::new(5, 12);
1304 let mut table = VirtualTable::new(twelve(), &source)
1305 .height(1)
1306 .frozen_columns(1)
1307 .column_offset(5)
1308 .visible_columns(2);
1309 assert_eq!(
1310 render(&table, 60),
1311 "\
1312┏━━━━━━┳━━━━━━┳━━━━━━┓
1313┃ c0 ┃ c6 ┃ c7 ┃
1314┡━━━━━━╇━━━━━━╇━━━━━━┩
1315│ r0c0 │ r0c6 │ r0c7 │
1316└──────┴──────┴──────┘
1317rows 1–1 of 5, columns 7–8 of 12
1318"
1319 );
1320 assert_eq!(source.seen(), vec![0, 6, 7]);
1321 table.scroll_columns_by(-100);
1323 assert_eq!(table.window_column_offset(), 0);
1324 table.scroll_columns_by(100);
1325 assert_eq!(table.max_column_offset(), 9);
1326 assert_eq!(table.window_column_offset(), 9);
1327 assert_eq!(table.shown_columns(60), vec![0, 10, 11]);
1328 table.set_visible_columns(Some(1));
1330 table.set_column_offset(50);
1331 assert_eq!(table.shown_columns(60), vec![0, 11]);
1332 assert_eq!(
1333 table.page().column_position(true).as_deref(),
1334 Some("column 12 of 12")
1335 );
1336 }
1337
1338 #[test]
1339 fn fit_to_width_shows_what_fits_from_the_offset() {
1340 let source = Counting::new(5, 12);
1341 let table = VirtualTable::new(twelve(), &source)
1342 .height(1)
1343 .frozen_columns(1)
1344 .column_offset(2)
1345 .fit_columns(true);
1346 assert_eq!(table.shown_columns(30), vec![0, 3, 4, 5]);
1349 assert_eq!(
1350 render(&table, 34),
1351 "\
1352┏━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1353┃ c0 ┃ c3 ┃ c4 ┃ c5 ┃
1354┡━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1355│ r0c0 │ r0c3 │ r0c4 │ r0c5 │
1356└──────┴──────┴──────┴──────┘
1357rows 1–1 of 5, columns 4–6 of 12
1358"
1359 );
1360 assert_eq!(source.seen(), vec![0, 3, 4, 5, 6]);
1362 assert_eq!(table.shown_columns(5), vec![0, 3]);
1364 let all = VirtualTable::new(twelve(), &source)
1366 .height(1)
1367 .fit_columns(true);
1368 assert_eq!(all.shown_columns(200).len(), 12);
1369 assert_eq!(all.page_at(200).column_position(false), None);
1370 let capped = VirtualTable::new(twelve(), &source)
1372 .fit_columns(true)
1373 .visible_columns(2);
1374 assert_eq!(capped.shown_columns(200), vec![0, 1]);
1375 }
1376
1377 #[test]
1378 fn a_window_reads_its_columns_aloud() {
1379 use crate::a11y::AccessibleText;
1380 let table = VirtualTable::new(twelve(), Counting::new(3, 12))
1381 .height(1)
1382 .column_offset(10);
1383 assert_eq!(
1384 table.accessible_text(80),
1385 "Table with 1 row, columns: c10, c11\nRow 1: c10: r0c10; c11: r0c11"
1386 );
1387 }
1388
1389 #[test]
1390 fn the_guides_wide_table() {
1391 let cells = FnCells::new(100_000, 2_000, |r, c| Value::from(r * c));
1392 let columns = (0..2_000).map(|c| Column::new(format!("c{c}")));
1393 let table = VirtualTable::new(columns, cells)
1394 .height(2)
1395 .frozen_columns(1)
1396 .column_offset(40)
1397 .fit_columns(true);
1398 assert_eq!(
1399 render(&table, 80),
1400 "\
1401┏━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1402┃ c0 ┃ c41 ┃ c42 ┃ c43 ┃ c44 ┃ c45 ┃ c46 ┃ c47 ┃ c48 ┃ c49 ┃ c50 ┃
1403┡━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1404│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
1405│ 0 │ 41 │ 42 │ 43 │ 44 │ 45 │ 46 │ 47 │ 48 │ 49 │ 50 │
1406└────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
1407rows 1–2 of 100,000, columns 42–51 of 2,000
1408"
1409 );
1410 }
1411
1412 #[test]
1413 fn fn_cells_compute_only_what_is_asked() {
1414 let calls = std::sync::atomic::AtomicUsize::new(0);
1415 let grid = FnCells::new(100_000, 2_000, |r, c| {
1416 calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1417 Value::from(r + c)
1418 });
1419 let table = VirtualTable::new((0..2_000).map(|c| Column::new(format!("{c}"))), &grid)
1420 .offset(50_000)
1421 .height(3)
1422 .column_offset(1_000)
1423 .visible_columns(3)
1424 .sample(10);
1425 let out = render(&table, 60);
1426 assert!(
1427 out.ends_with("rows 50,001–50,003 of 100,000, columns 1,001–1,003 of 2,000\n"),
1428 "{out}"
1429 );
1430 assert_eq!(
1432 calls.load(std::sync::atomic::Ordering::Relaxed),
1433 (10 + 3) * 3
1434 );
1435 assert_eq!(grid.row(3).map(|r| r.len()), Some(2_000));
1436 assert_eq!(grid.rows_columns(99_999, 5, &[0, 5_000]).len(), 1);
1437 assert_eq!(grid.rows_columns(0, 1, &[5_000]), vec![vec![Value::Null]]);
1438 }
1439}