Skip to main content

gpui_kit_grid/state/
export.rs

1//! Exporting the view to a stream or a file in the background, and what a
2//! copy and an export take when their command runs ([`ExportCapture`]).
3//!
4//! An export is taken when the command runs, as a copy is: the snapshot, the
5//! shown order (a held one included) with its expansion, the columns with
6//! their metadata, the totals label and the optimistic values of waiting
7//! edits. Rows that arrive or change later are not in it. A task runner job
8//! (by default in the background; on the web, one task on the main thread)
9//! builds the table and hands it to an [`ExportFormat`] or to the
10//! application's closure. No delegate hook runs and no event is emitted:
11//! the application awaits the [`ExportJob`].
12
13use std::{
14    future::Future,
15    io,
16    pin::Pin,
17    sync::Arc,
18    task::{Context as TaskContext, Poll},
19};
20
21use gpui_kit::{Context, SharedString, Task};
22use rust_i18n::t;
23
24use super::{
25    ColumnOf, LiveGridDelegate, LiveGridState, columns::column_of, pending::PendingValues,
26    selection::ScopedSelection,
27};
28use crate::{
29    engine::{DisplayRow, Projection},
30    export::{
31        CellExport, ExportColumn, ExportControl, ExportError, ExportFormat, ExportLine, ExportRow,
32        ExportScope, ExportTable,
33    },
34    model::{ColumnId, ColumnIx, ColumnSet, GridColumns as _, GridRow, NumberFormat},
35    state::{ColumnAlign, ColumnZone},
36    store::{PinnedEdge, StoreSnapshot},
37};
38
39/// What an export takes from the grid: the scope, the header line, the
40/// columns and how a text cell is written.
41///
42/// ```
43/// use gpui_kit_grid::{EmptyExport, ExportHeader, ExportRequest, ExportScope, GridRow};
44///
45/// /// The whole view, every group and tree row expanded, without a header
46/// /// line; a file of the header line alone when there is nothing to export.
47/// fn everything<R: GridRow>() -> ExportRequest<R> {
48///     ExportRequest::new(ExportScope::Expanded)
49///         .with_header(ExportHeader::None)
50///         .with_empty(EmptyExport::HeaderOnly)
51/// }
52/// ```
53pub struct ExportRequest<R: GridRow> {
54    scope: ExportScope,
55    header: ExportHeader,
56    empty: EmptyExport,
57    columns: Option<Vec<R::Column>>,
58    cells: Option<CellExport<R>>,
59}
60
61impl<R: GridRow> ExportRequest<R> {
62    /// An export of `scope` with a header line, the visible columns in
63    /// screen order and the grid's [`CellExport`]
64    /// (`LiveGridState::with_cell_export`).
65    pub fn new(scope: ExportScope) -> Self {
66        Self {
67            scope,
68            header: ExportHeader::default(),
69            empty: EmptyExport::default(),
70            columns: None,
71            cells: None,
72        }
73    }
74
75    /// Sets the header lines (default `Titles`, whatever the scope; a copy
76    /// of the selection has none).
77    pub fn with_header(self, header: ExportHeader) -> Self {
78        Self { header, ..self }
79    }
80
81    /// Sets what an export of an empty scope does (default
82    /// `EmptyExport::Nothing`).
83    pub fn with_empty(self, empty: EmptyExport) -> Self {
84        Self { empty, ..self }
85    }
86
87    /// Exports these columns in this order, hidden ones included; columns
88    /// the schema does not hold are left out. By default: the visible
89    /// columns in screen order; for `ExportScope::Selection` in a cell
90    /// mode, the visible columns that hold a selected cell.
91    pub fn with_columns(self, columns: impl IntoIterator<Item = R::Column>) -> Self {
92        Self {
93            columns: Some(columns.into_iter().collect()),
94            ..self
95        }
96    }
97
98    /// Sets how a text format writes a cell (default: the grid's).
99    pub fn with_cell_export(self, cells: CellExport<R>) -> Self {
100        Self {
101            cells: Some(cells),
102            ..self
103        }
104    }
105
106    /// The rows the export takes.
107    pub fn scope(&self) -> ExportScope {
108        self.scope
109    }
110
111    /// The header lines.
112    pub fn header(&self) -> ExportHeader {
113        self.header
114    }
115
116    /// What an export of an empty scope does.
117    pub fn empty(&self) -> EmptyExport {
118        self.empty
119    }
120}
121
122impl<R: GridRow> Default for ExportRequest<R> {
123    /// The visible lines: `ExportScope::Visible`.
124    fn default() -> Self {
125        Self::new(ExportScope::Visible)
126    }
127}
128
129impl<R: GridRow> Clone for ExportRequest<R> {
130    fn clone(&self) -> Self {
131        Self {
132            scope: self.scope,
133            header: self.header,
134            empty: self.empty,
135            columns: self.columns.clone(),
136            cells: self.cells.clone(),
137        }
138    }
139}
140
141/// The header lines of an export. An enum, not a flag: the band titles
142/// above the column titles would be one more variant.
143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum ExportHeader {
146    /// No header line.
147    None,
148    /// One line of column titles.
149    #[default]
150    Titles,
151}
152
153/// What an export of an empty scope does.
154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum EmptyExport {
157    /// `Err(ExportError::Nothing)` at once: nothing is written and no file
158    /// is created, as a copy leaves the clipboard alone.
159    #[default]
160    Nothing,
161    /// The header line alone, as AG Grid writes it.
162    HeaderOnly,
163}
164
165/// A running export: its progress, its cancellation and, as a `Future`, its
166/// result.
167///
168/// Dropping it cancels the export: the format stops at its next
169/// [`ExportControl::advance`], and a file being written to a path is
170/// removed. [`detach`](Self::detach) lets it run to the end with nobody
171/// waiting. On the web the export runs as one task on the main thread: its
172/// progress goes from nothing to done at once, and a cancellation takes
173/// effect only before the task starts.
174#[must_use = "dropping an export job cancels the export"]
175pub struct ExportJob<T> {
176    control: ExportControl,
177    /// The job's task: dropping it before the job starts keeps it from
178    /// running.
179    task: Option<Task<()>>,
180    /// Closed once the job has run or was dropped. Boxed: a receiver is
181    /// not `Unpin`, the job is.
182    done: Box<async_channel::Receiver<Result<ExportOutcome<T>, ExportError>>>,
183    /// Waits for the result.
184    result: Pin<Box<dyn Future<Output = Result<ExportOutcome<T>, ExportError>> + Send>>,
185    detached: bool,
186}
187
188impl<T: Send + 'static> ExportJob<T> {
189    fn new(
190        control: ExportControl,
191        task: Task<()>,
192        done: async_channel::Receiver<Result<ExportOutcome<T>, ExportError>>,
193    ) -> Self {
194        let result = {
195            let done = done.clone();
196            Box::pin(async move {
197                // A job dropped without running sends nothing.
198                done.recv().await.unwrap_or(Err(ExportError::Cancelled))
199            })
200        };
201        Self {
202            control,
203            task: Some(task),
204            done: Box::new(done),
205            result,
206            detached: false,
207        }
208    }
209}
210
211impl<T> ExportJob<T> {
212    /// The rows written of the rows to write: lock-free, for a status bar
213    /// or `GridProgress::new(progress.fraction())`.
214    pub fn progress(&self) -> ExportProgress {
215        ExportProgress {
216            rows_written: self.control.rows_written(),
217            rows_total: self.control.rows_total(),
218        }
219    }
220
221    /// Cancels the export: the job's result is `Err(ExportError::Cancelled)`
222    /// unless it has already finished.
223    pub fn cancel(&self) {
224        self.control.cancel();
225    }
226
227    /// Returns true if the job has finished, whatever its result.
228    pub fn is_finished(&self) -> bool {
229        self.done.is_closed()
230    }
231
232    /// Lets the export run to the end with nobody waiting for its result.
233    pub fn detach(mut self) {
234        if let Some(task) = self.task.take() {
235            task.detach();
236        }
237        self.detached = true;
238    }
239}
240
241impl<T> Future for ExportJob<T> {
242    type Output = Result<ExportOutcome<T>, ExportError>;
243
244    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
245        let this = self.get_mut();
246        let result = std::task::ready!(this.result.as_mut().poll(cx));
247        this.task = None;
248        Poll::Ready(result)
249    }
250}
251
252impl<T> Drop for ExportJob<T> {
253    fn drop(&mut self) {
254        if !self.detached {
255            self.control.cancel();
256        }
257    }
258}
259
260/// The rows an export has written of the rows it writes.
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub struct ExportProgress {
263    rows_written: usize,
264    rows_total: usize,
265}
266
267impl ExportProgress {
268    /// The rows written so far.
269    pub fn rows_written(&self) -> usize {
270        self.rows_written
271    }
272
273    /// The rows to write: counted ahead when the export starts, exactly
274    /// once its job has built the table.
275    pub fn rows_total(&self) -> usize {
276        self.rows_total
277    }
278
279    /// The share written, from 0.0 to 1.0; 1.0 with no row to write.
280    pub fn fraction(&self) -> f32 {
281        if self.rows_total == 0 {
282            return 1.0;
283        }
284        (self.rows_written.min(self.rows_total) as f64 / self.rows_total as f64) as f32
285    }
286}
287
288/// A finished export: what it made, and how much it wrote.
289#[derive(Clone, Debug)]
290pub struct ExportOutcome<T> {
291    output: T,
292    rows: usize,
293    data_rows: usize,
294    bytes: Option<u64>,
295}
296
297impl<T> ExportOutcome<T> {
298    /// What the export made: the sink of `export`, the path of
299    /// `export_to_path`, the value of the closure of `export_with`.
300    pub fn output(&self) -> &T {
301        &self.output
302    }
303
304    /// Takes what the export made.
305    pub fn into_output(self) -> T {
306        self.output
307    }
308
309    /// The rows of the table, summary and band rows included, the header
310    /// line not.
311    pub fn rows(&self) -> usize {
312        self.rows
313    }
314
315    /// The application's rows among them.
316    pub fn data_rows(&self) -> usize {
317        self.data_rows
318    }
319
320    /// The bytes written to the sink or the file; `None` for `export_with`.
321    pub fn bytes(&self) -> Option<u64> {
322        self.bytes
323    }
324}
325
326/// The rows a copy or an export writes, in the order they are shown.
327enum CaptureRows<K> {
328    /// The shown lines ([`ExportScope::Visible`]): group captions, the data
329    /// rows of expanded groups and tree rows, group footers. The bands go
330    /// around them.
331    Lines(Arc<Projection>),
332    /// The lines as if every group and tree row were expanded
333    /// ([`ExportScope::Expanded`]), those the screen hides marked. The bands
334    /// go around them.
335    Expanded(Arc<Projection>),
336    /// The command scope: rows by key, each with its selected columns.
337    Scoped(ScopedSelection<K>),
338}
339
340impl<K> CaptureRows<K> {
341    /// The number of lines, before rows removed since are left out; at most
342    /// that for `Expanded`, whose footers are not counted ahead.
343    fn len(&self) -> usize {
344        match self {
345            Self::Lines(projection) => projection.len(),
346            Self::Expanded(projection) => projection
347                .rows()
348                .len()
349                .saturating_add(projection.groups().len().saturating_mul(2)),
350            Self::Scoped(scoped) => scoped.len(),
351        }
352    }
353}
354
355/// An exported column as the grid showed it when the command ran.
356struct CapturedColumn<C> {
357    column: C,
358    title: SharedString,
359    width: f32,
360    align: ColumnAlign,
361    zone: ColumnZone,
362    /// The titles of the bands over it, outermost first.
363    bands: Box<[SharedString]>,
364}
365
366/// The pinned rows of both bands of `snapshot`.
367fn pinned_count<R: GridRow>(snapshot: &StoreSnapshot<R>) -> usize {
368    snapshot.pinned_rows(PinnedEdge::Top).len() + snapshot.pinned_rows(PinnedEdge::Bottom).len()
369}
370
371/// One copy or export, as taken when its command ran: everything its table
372/// is built from. Moves to the job that writes it.
373pub(super) struct ExportCapture<R: GridRow> {
374    snapshot: Arc<StoreSnapshot<R>>,
375    rows: CaptureRows<R::Key>,
376    /// The exported columns in order.
377    columns: Vec<CapturedColumn<R::Column>>,
378    header: bool,
379    /// The column of a caption's key value, by the id of its grouping
380    /// level: the level's own column, for the column levels that are
381    /// exported. Any other caption's value goes to the first exported
382    /// column.
383    value_columns: Vec<(ColumnId, R::Column)>,
384    /// The band of the totals row and its label, translated when the
385    /// command ran; `None`: no totals row.
386    totals: Option<(PinnedEdge, SharedString)>,
387    /// The optimistic values of the waiting edits: a cell is copied and
388    /// exported as the screen shows it.
389    pending: Arc<PendingValues<R::Key, R::Column>>,
390    /// How each column shows its numbers, by `ColumnIx`: a cell is
391    /// written as the screen shows it.
392    formats: Arc<[NumberFormat]>,
393    cells: CellExport<R>,
394    /// No row or no column to write.
395    is_empty: bool,
396}
397
398impl<R: GridRow> ExportCapture<R> {
399    /// Returns true if there is nothing to write: no line, pinned row or
400    /// totals row in the scope, or no column.
401    pub(super) fn is_empty(&self) -> bool {
402        self.is_empty
403    }
404
405    /// The rows of the table, counted ahead: the lines (at most, for
406    /// `Expanded`) and the bands.
407    pub(super) fn row_estimate(&self) -> usize {
408        let bands = match self.rows {
409            CaptureRows::Lines(_) | CaptureRows::Expanded(_) => {
410                pinned_count(&self.snapshot) + usize::from(self.totals.is_some())
411            }
412            CaptureRows::Scoped(_) => 0,
413        };
414        self.rows.len().saturating_add(bands)
415    }
416
417    /// The cells of the table, counted ahead.
418    pub(super) fn cell_count(&self) -> usize {
419        self.row_estimate().saturating_mul(self.columns.len())
420    }
421
422    /// Builds the table and hands it to `write` with the cell export. A
423    /// line whose row the snapshot no longer has is left out, so the table
424    /// may have no row even when the capture was not empty.
425    pub(super) fn write<T>(
426        &self,
427        write: impl FnOnce(&ExportTable<'_, R>, &CellExport<R>) -> T,
428    ) -> T {
429        let band_titles: Vec<&str> = self
430            .columns
431            .iter()
432            .flat_map(|column| column.bands.iter().map(SharedString::as_ref))
433            .collect();
434        let mut band_start = 0;
435        let columns: Vec<ExportColumn<'_, R::Column>> = self
436            .columns
437            .iter()
438            .map(|column| {
439                let bands = &band_titles[band_start..band_start + column.bands.len()];
440                band_start += column.bands.len();
441                ExportColumn::new(column.column, &column.title)
442                    .with_width(column.width)
443                    .with_align(column.align)
444                    .with_zone(column.zone)
445                    .with_bands(bands)
446            })
447            .collect();
448        let mut rows = self.rows();
449        for row in &mut rows {
450            *row = row.with_number_formats(&self.formats);
451        }
452        write(
453            &ExportTable::new(&columns, &rows).with_header(self.header),
454            &self.cells,
455        )
456    }
457
458    /// The rows of the table, in the shown order.
459    fn rows(&self) -> Vec<ExportRow<'_, R>> {
460        let exported = self
461            .columns
462            .iter()
463            .fold(ColumnSet::EMPTY, |mut set, column| {
464                set.insert(column.column.ix());
465                set
466            });
467        match &self.rows {
468            CaptureRows::Lines(projection) => self.view_rows(
469                projection,
470                (0..projection.len()).map_while(|ix| projection.line(ix)),
471                false,
472            ),
473            CaptureRows::Expanded(projection) => {
474                self.view_rows(projection, projection.expanded_lines(), true)
475            }
476            // The scope was resolved on this snapshot, so every key is in it.
477            // A row whose selected cells are all left out has nothing to
478            // write.
479            CaptureRows::Scoped(scoped) => scoped
480                .keys()
481                .iter()
482                .enumerate()
483                .filter_map(|(ix, key)| {
484                    let selected = scoped.columns(ix);
485                    if !selected.intersects(exported) {
486                        return None;
487                    }
488                    let row = self.snapshot.get(key)?;
489                    Some(self.data_row(row).with_columns(selected))
490                })
491                .collect(),
492        }
493    }
494
495    /// A data row with the optimistic values of its waiting edits: one
496    /// lookup, and none while no edit waits.
497    fn data_row<'a>(&'a self, row: &'a R) -> ExportRow<'a, R> {
498        let export = ExportRow::new(row);
499        if self.pending.is_empty() {
500            return export;
501        }
502        match self.pending.get(row.key()) {
503            Some(values) => export.with_pending(values),
504            None => export,
505        }
506    }
507
508    /// The rows of the view: the top band (the totals row, the outermost,
509    /// then the pinned rows), `lines` of `projection`, the bottom band (the
510    /// pinned rows, then the totals row). The band rows carry their edge,
511    /// tree rows their branch; with `mark_hidden`, the lines the shown
512    /// expansion hides are marked.
513    ///
514    /// A data line whose row the snapshot no longer has, removed or fading,
515    /// is left out: a shown order built before a removal still holds it.
516    fn view_rows<'a>(
517        &'a self,
518        projection: &'a Projection,
519        lines: impl Iterator<Item = DisplayRow> + 'a,
520        mark_hidden: bool,
521    ) -> Vec<ExportRow<'a, R>> {
522        let snapshot = &*self.snapshot;
523        let groups = projection.groups();
524        let is_tree = projection.is_tree();
525        let first_column = self.columns[0].column;
526        let totals = |edge| {
527            self.totals
528                .as_ref()
529                .filter(|(at, _)| *at == edge)
530                .map(|(_, label)| {
531                    ExportRow::totals(projection.totals(), Some((first_column, label.as_ref())))
532                        .with_band(edge)
533                })
534        };
535        // Pinned rows have keys of their own: no edit waits on them.
536        let pinned = |edge| {
537            snapshot
538                .pinned_rows(edge)
539                .iter()
540                .map(move |row| ExportRow::new(&**row).with_band(edge))
541        };
542
543        let mut hidden = HiddenLines::default();
544        let mut rows = Vec::with_capacity(self.rows.len() + pinned_count(snapshot) + 1);
545        rows.extend(totals(PinnedEdge::Top));
546        rows.extend(pinned(PinnedEdge::Top));
547        rows.extend(lines.filter_map(|line| {
548            let (row, is_hidden) = match line {
549                DisplayRow::Data { row: id, depth } => {
550                    let branch = if is_tree { projection.branch(id) } else { None };
551                    let collapsed = branch.is_some_and(|branch| !branch.is_expanded());
552                    let is_hidden = mark_hidden && hidden.next(depth, collapsed, false);
553                    let row = self.data_row(snapshot.row(id)?).with_depth(depth);
554                    (
555                        match branch {
556                            Some(branch) => row.with_branch(branch),
557                            None => row,
558                        },
559                        is_hidden,
560                    )
561                }
562                DisplayRow::GroupCaption { group, depth } => {
563                    let group = groups.get(group)?;
564                    let is_hidden = mark_hidden && hidden.next(depth, !group.is_expanded(), false);
565                    let level = group.level_id();
566                    let value_column = self
567                        .value_columns
568                        .iter()
569                        .find(|(id, _)| id == level)
570                        .map_or(first_column, |(_, column)| *column);
571                    (ExportRow::caption(value_column, group), is_hidden)
572                }
573                DisplayRow::GroupFooter { group, depth } => {
574                    let group = groups.get(group)?;
575                    let is_hidden = mark_hidden && hidden.next(depth, false, !group.is_expanded());
576                    (ExportRow::footer(group), is_hidden)
577                }
578            };
579            Some(row.with_hidden(is_hidden))
580        }));
581        rows.extend(pinned(PinnedEdge::Bottom));
582        rows.extend(totals(PinnedEdge::Bottom));
583        rows
584    }
585
586    /// Writes the table with `write` and counts what was written, in a job.
587    /// `Err(Cancelled)` if the export was cancelled before the job started;
588    /// `Err(Nothing)` if the table has no row and `empty` asks for nothing.
589    fn run<T>(
590        &self,
591        empty: EmptyExport,
592        control: &ExportControl,
593        write: impl FnOnce(
594            &ExportTable<'_, R>,
595            &CellExport<R>,
596            &ExportControl,
597        ) -> Result<(T, Option<u64>), ExportError>,
598    ) -> Result<ExportOutcome<T>, ExportError> {
599        control.advance(0)?;
600        self.write(|table, cells| {
601            let rows = table.rows().len();
602            control.set_rows_total(rows);
603            if rows == 0 && empty == EmptyExport::Nothing {
604                return Err(ExportError::Nothing);
605            }
606            let (output, bytes) = write(table, cells, control)?;
607            Ok(ExportOutcome {
608                output,
609                rows,
610                data_rows: table
611                    .rows()
612                    .iter()
613                    .filter(|row| row.line() == ExportLine::Data)
614                    .count(),
615                bytes,
616            })
617        })
618    }
619}
620
621/// Marks the lines of an expanded walk that the shown expansion hides: the
622/// lines deeper than a collapsed caption or tree row after it, and the
623/// footer of a collapsed group.
624#[derive(Default)]
625struct HiddenLines {
626    /// The depth of the shown collapsed line whose deeper lines are hidden.
627    below: Option<u16>,
628}
629
630impl HiddenLines {
631    /// Returns true if the next line, at `depth`, is hidden. `collapsed`:
632    /// the line hides the deeper lines after it; `collapsed_footer`: it is
633    /// the footer of a collapsed group.
634    fn next(&mut self, depth: u16, collapsed: bool, collapsed_footer: bool) -> bool {
635        let under_collapsed = match self.below {
636            Some(below) if depth > below => true,
637            _ => {
638                self.below = None;
639                false
640            }
641        };
642        if !under_collapsed && collapsed {
643            self.below = Some(depth);
644        }
645        under_collapsed || collapsed_footer
646    }
647}
648
649/// A sink that counts the bytes written to it.
650struct CountingWriter<W> {
651    inner: W,
652    bytes: u64,
653}
654
655impl<W: io::Write> io::Write for CountingWriter<W> {
656    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
657        let written = self.inner.write(buf)?;
658        self.bytes += written as u64;
659        Ok(written)
660    }
661
662    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
663        self.inner.write_all(buf)?;
664        self.bytes += buf.len() as u64;
665        Ok(())
666    }
667
668    fn flush(&mut self) -> io::Result<()> {
669        self.inner.flush()
670    }
671}
672
673/// Writes a file through `<path>.part` beside it, renamed over `path` once
674/// complete: a cancelled or failed export removes the part file and leaves
675/// `path` as it was. Returns the bytes written.
676#[cfg(not(target_family = "wasm"))]
677fn write_through_part_file(
678    path: &std::path::Path,
679    control: &ExportControl,
680    write: impl FnOnce(&mut dyn io::Write) -> Result<(), ExportError>,
681) -> Result<u64, ExportError> {
682    let Some(name) = path.file_name() else {
683        return Err(
684            io::Error::new(io::ErrorKind::InvalidInput, "the export path names no file").into(),
685        );
686    };
687    let mut part_name = name.to_os_string();
688    part_name.push(".part");
689    let part = path.with_file_name(part_name);
690    let written = (|| {
691        let file = std::fs::File::create(&part)?;
692        let mut out = CountingWriter {
693            inner: io::BufWriter::new(file),
694            bytes: 0,
695        };
696        write(&mut out)?;
697        let bytes = out.bytes;
698        let file = out
699            .inner
700            .into_inner()
701            .map_err(io::IntoInnerError::into_error)?;
702        file.sync_all()?;
703        drop(file);
704        // Cancelled after its last row: `path` still stays as it was.
705        control.advance(0)?;
706        std::fs::rename(&part, path)?;
707        Ok(bytes)
708    })();
709    if written.is_err() {
710        let _ = std::fs::remove_file(&part);
711    }
712    written
713}
714
715impl<D: LiveGridDelegate> LiveGridState<D> {
716    /// Writes `request` with `format` to `sink`, in a task runner job, and
717    /// gives the sink back when it is done. The snapshot, the shown order (a
718    /// held one included) with its expansion, the columns, the totals label
719    /// and the optimistic values of waiting edits are taken now.
720    ///
721    /// Returns `Err(ExportError::Nothing)` at once, without touching the
722    /// sink, if the scope is empty ([`Self::is_export_empty`]), unless the
723    /// request asks for the header line alone (`EmptyExport::HeaderOnly`).
724    /// The job writes on the web too: a `Vec<u8>` sink there holds the
725    /// bytes of the file the application lets the user save.
726    ///
727    /// ```
728    /// use gpui_kit::{Context, Task};
729    /// use gpui_kit_grid::{
730    ///     CsvFormat, ExportError, ExportRequest, ExportScope, LiveGridDelegate, LiveGridState,
731    /// };
732    ///
733    /// /// The CSV text of the visible lines, once it is written.
734    /// fn csv_text<D: LiveGridDelegate>(
735    ///     grid: &mut LiveGridState<D>,
736    ///     cx: &mut Context<LiveGridState<D>>,
737    /// ) -> Result<Task<Result<Vec<u8>, ExportError>>, ExportError> {
738    ///     let job = grid.export(
739    ///         ExportRequest::new(ExportScope::Visible),
740    ///         CsvFormat::new(),
741    ///         Vec::new(),
742    ///         cx,
743    ///     )?;
744    ///     Ok(cx.spawn(async move |_, _| Ok(job.await?.into_output())))
745    /// }
746    /// ```
747    pub fn export<W: io::Write + Send + 'static>(
748        &mut self,
749        request: ExportRequest<D::Row>,
750        format: impl ExportFormat<D::Row>,
751        sink: W,
752        cx: &mut Context<Self>,
753    ) -> Result<ExportJob<W>, ExportError> {
754        self.start_export(
755            request,
756            move |table, cells, control| {
757                let mut out = CountingWriter {
758                    inner: sink,
759                    bytes: 0,
760                };
761                format.export(table, cells, &mut out, control)?;
762                Ok((out.inner, Some(out.bytes)))
763            },
764            cx,
765        )
766    }
767
768    /// As [`Self::export`], to a file at `path`: written to `<path>.part`
769    /// beside it, then renamed over `path`. A cancelled or failed export
770    /// removes the part file and leaves `path` as it was; on Windows a file
771    /// another program holds open fails the rename with `ExportError::Io`.
772    /// Not on the web, which has no file system: export to a `Vec<u8>` there.
773    ///
774    /// The application asks for the path first (`App::prompt_for_new_path`
775    /// with the format's extension) and may open the file once the job is
776    /// done (`App::open_with_system`).
777    #[cfg(not(target_family = "wasm"))]
778    pub fn export_to_path(
779        &mut self,
780        request: ExportRequest<D::Row>,
781        format: impl ExportFormat<D::Row>,
782        path: impl Into<std::path::PathBuf>,
783        cx: &mut Context<Self>,
784    ) -> Result<ExportJob<std::path::PathBuf>, ExportError> {
785        let path = path.into();
786        self.start_export(
787            request,
788            move |table, cells, control| {
789                let bytes = write_through_part_file(&path, control, |out| {
790                    format.export(table, cells, out, control)
791                })?;
792                Ok((path, Some(bytes)))
793            },
794            cx,
795        )
796    }
797
798    /// Hands the export table to `write` in a task runner job: an
799    /// application builds its own file there, such as an xlsx workbook with
800    /// its own library, and returns what it made. `write` calls
801    /// [`ExportControl::advance`] as an [`ExportFormat`] does. What is taken
802    /// now and when nothing is exported: as [`Self::export`].
803    pub fn export_with<T: Send + 'static>(
804        &mut self,
805        request: ExportRequest<D::Row>,
806        write: impl FnOnce(
807            &ExportTable<'_, D::Row>,
808            &CellExport<D::Row>,
809            &ExportControl,
810        ) -> Result<T, ExportError>
811        + Send
812        + 'static,
813        cx: &mut Context<Self>,
814    ) -> Result<ExportJob<T>, ExportError> {
815        self.start_export(
816            request,
817            move |table, cells, control| Ok((write(table, cells, control)?, None)),
818            cx,
819        )
820    }
821
822    /// Returns true if an export of `scope` would have nothing to write
823    /// (`ExportError::Nothing`): no line, pinned row or totals row with
824    /// aggregates for `Visible` and `Expanded`, an empty command scope for
825    /// `Selection`. Check it before asking for a file name.
826    pub fn is_export_empty(&self, scope: ExportScope) -> bool {
827        self.capture(scope, None, true, CellExport::default())
828            .is_empty()
829    }
830
831    /// Takes the export of `request` and starts its job.
832    fn start_export<T: Send + 'static>(
833        &mut self,
834        request: ExportRequest<D::Row>,
835        write: impl FnOnce(
836            &ExportTable<'_, D::Row>,
837            &CellExport<D::Row>,
838            &ExportControl,
839        ) -> Result<(T, Option<u64>), ExportError>
840        + Send
841        + 'static,
842        cx: &mut Context<Self>,
843    ) -> Result<ExportJob<T>, ExportError> {
844        let ExportRequest {
845            scope,
846            header,
847            empty,
848            columns,
849            cells,
850        } = request;
851        let cells = cells.unwrap_or_else(|| self.copy_cells().clone());
852        let capture = self.capture(
853            scope,
854            columns.as_deref(),
855            header == ExportHeader::Titles,
856            cells,
857        );
858        if capture.is_empty() && (empty == EmptyExport::Nothing || capture.columns.is_empty()) {
859            return Err(ExportError::Nothing);
860        }
861        let control = ExportControl::new(capture.row_estimate());
862        let (done, result) = async_channel::bounded(1);
863        let job_control = control.clone();
864        let task = self.runner.run(
865            Box::new(move || {
866                let _ = done.try_send(capture.run(empty, &job_control, write));
867            }),
868            cx,
869        );
870        Ok(ExportJob::new(control, task, result))
871    }
872
873    /// What a copy or an export of `scope` takes now: the snapshot, the
874    /// shown order or the command scope, `columns` (by default the visible
875    /// columns in screen order; for `Selection`, those that hold a selected
876    /// cell) with their metadata, the summaries of a view scope and the
877    /// optimistic values. O(columns), and O(selection) for `Selection`;
878    /// the rows are walked in the job.
879    pub(super) fn capture(
880        &self,
881        scope: ExportScope,
882        columns: Option<&[ColumnOf<D>]>,
883        header: bool,
884        cells: CellExport<D::Row>,
885    ) -> ExportCapture<D::Row> {
886        let (rows, selected) = match scope {
887            ExportScope::Selection => {
888                let scoped = self.command_scope(self.focus_target().as_ref());
889                let selected = (0..scoped.len()).fold(ColumnSet::EMPTY, |mut set, ix| {
890                    set.union_with(scoped.columns(ix));
891                    set
892                });
893                (CaptureRows::Scoped(scoped), selected)
894            }
895            ExportScope::Visible => (
896                CaptureRows::Lines(self.projection.clone()),
897                self.columns.visible_set(),
898            ),
899            ExportScope::Expanded => (
900                CaptureRows::Expanded(self.projection.clone()),
901                self.columns.visible_set(),
902            ),
903        };
904        let columns: Vec<CapturedColumn<ColumnOf<D>>> = match columns {
905            Some(columns) => columns
906                .iter()
907                .filter_map(|column| self.captured_column(column.ix()))
908                .collect(),
909            None => {
910                // An empty scope: the header line of every visible column.
911                let selected = if selected.is_empty() {
912                    self.columns.visible_set()
913                } else {
914                    selected
915                };
916                self.columns
917                    .visible()
918                    .filter(|ix| selected.contains(*ix))
919                    .filter_map(|ix| self.captured_column(ix))
920                    .collect()
921            }
922        };
923        let (value_columns, totals) = match rows {
924            CaptureRows::Scoped(_) => (Vec::new(), None),
925            CaptureRows::Lines(_) | CaptureRows::Expanded(_) => {
926                let value_columns = self
927                    .view
928                    .group_by()
929                    .iter()
930                    .filter_map(|level| {
931                        let column = level.as_column()?;
932                        let exported = columns.iter().any(|each| each.column == column);
933                        exported.then(|| (level.id(), column))
934                    })
935                    .collect();
936                // A totals row without aggregates holds only its label:
937                // nothing to write.
938                let totals = self
939                    .totals()
940                    .filter(|_| !self.projection.totals().is_empty())
941                    .map(|edge| {
942                        let label = SharedString::from(t!("live_grid.totals.label").into_owned());
943                        (edge, label)
944                    });
945                (value_columns, totals)
946            }
947        };
948        let no_rows = match &rows {
949            CaptureRows::Scoped(scoped) => scoped.is_empty(),
950            CaptureRows::Lines(projection) | CaptureRows::Expanded(projection) => {
951                projection.is_empty() && pinned_count(&self.snapshot) == 0 && totals.is_none()
952            }
953        };
954        ExportCapture {
955            snapshot: self.snapshot.clone(),
956            rows,
957            is_empty: no_rows || columns.is_empty(),
958            columns,
959            header,
960            value_columns,
961            totals,
962            pending: self.pending_values(),
963            formats: self.columns.number_formats(),
964            cells,
965        }
966    }
967
968    /// The column `ix` as the grid shows it now; `None` outside the schema.
969    fn captured_column(&self, ix: ColumnIx) -> Option<CapturedColumn<ColumnOf<D>>> {
970        let column = self.columns.get(ix)?;
971        Some(CapturedColumn {
972            column: column_of(ix),
973            title: column.title().clone(),
974            width: f32::from(self.columns.width(ix)),
975            align: column.align(),
976            zone: self.columns.zone_of(ix),
977            bands: self
978                .columns
979                .bands_of(ix)
980                .iter()
981                .map(|band| {
982                    self.columns
983                        .band_title(band)
984                        .cloned()
985                        .unwrap_or_else(|| SharedString::new_static(band))
986                })
987                .collect(),
988        })
989    }
990}
991
992#[cfg(test)]
993mod tests {
994    use super::*;
995
996    #[test]
997    fn test_hidden_lines() {
998        // A collapsed caption at depth 0 hides its rows and subgroups, and
999        // its footer; the next top-level caption is shown.
1000        let mut hidden = HiddenLines::default();
1001        let walk = [
1002            // (depth, collapsed, collapsed footer, hidden)
1003            (0, true, false, false),
1004            (1, false, false, true),
1005            (2, false, false, true),
1006            (1, true, false, true),
1007            (2, false, false, true),
1008            (1, false, true, true),
1009            (0, false, true, true),
1010            (0, false, false, false),
1011            // An expanded caption with a collapsed subgroup.
1012            (1, true, false, false),
1013            (2, false, false, true),
1014            (1, false, true, true),
1015            (1, false, false, false),
1016            (2, false, false, false),
1017            (1, false, false, false),
1018            (0, false, false, false),
1019        ];
1020        for (ix, (depth, collapsed, collapsed_footer, expected)) in walk.into_iter().enumerate() {
1021            assert_eq!(
1022                hidden.next(depth, collapsed, collapsed_footer),
1023                expected,
1024                "line {ix}"
1025            );
1026        }
1027    }
1028
1029    #[test]
1030    fn test_progress_fraction() {
1031        let progress = |rows_written, rows_total| ExportProgress {
1032            rows_written,
1033            rows_total,
1034        };
1035        assert_eq!(progress(0, 300).fraction(), 0.0);
1036        assert_eq!(progress(150, 300).fraction(), 0.5);
1037        assert_eq!(progress(300, 300).fraction(), 1.0);
1038        // A format that counts more than the table: done, not more.
1039        assert_eq!(progress(400, 300).fraction(), 1.0);
1040        // Nothing to write is done.
1041        assert_eq!(progress(0, 0).fraction(), 1.0);
1042    }
1043}