use gpui_kit::prelude::*;
use gpui_kit::{App, ClipboardItem, Context, Window};
use crate::db::Engine;
use crate::db::export::{self, Format};
use crate::db::query::{self, Cell};
use super::{Copied, CopyAs, CopyValue, CopyWithHeaders, DataGrid, Scope, Snapshot};
impl DataGrid {
pub(super) fn on_copy_value(
&mut self,
_: &CopyValue,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.copy_selection(cx);
}
pub(super) fn on_copy_with_headers(
&mut self,
_: &CopyWithHeaders,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.copy_as(CopyAs::Rows(Format::Tsv), cx);
}
pub fn copy_selection(&mut self, cx: &mut Context<Self>) {
self.copy(None, cx);
}
pub(super) fn copy(&mut self, cell: Option<(usize, usize)>, cx: &mut Context<Self>) {
self.commit_editor(cx);
if let Some((row_ix, col_ix)) = cell {
self.copy_cell(row_ix, col_ix, cx);
return;
}
if !self.table.read(cx).delegate().rows_selected.is_empty() {
let (snapshot, total) = self.snapshot_capped(Scope::Picked, MAX_COPY_ROWS, cx);
let rows = snapshot.rows.len();
let mut skipped = 0;
let text = snapshot
.rows
.iter()
.map(|row| {
row.iter()
.map(|cell| {
let (text, read_back) = cell_text(cell);
skipped += usize::from(!read_back);
crate::db::export::tsv_field(&text)
})
.collect::<Vec<String>>()
.join("\t")
})
.collect::<Vec<String>>()
.join("\n");
let (text, capped_bytes) = cap_bytes(text);
let message = if rows < total {
format!("Copied first {rows} of {total} rows")
} else if capped_bytes {
format!("Copied the first {} MB", MAX_COPY_BYTES / (1024 * 1024))
} else {
format!("Copied {}", count_of(rows, "row", "rows"))
};
self.put_on_clipboard(text, with_skipped(message, skipped), cx);
return;
}
let Some((row_ix, col_ix)) = self.selected_cell(cx) else {
return;
};
self.copy_cell(row_ix, col_ix, cx);
}
fn copy_cell(&mut self, row_ix: usize, col_ix: usize, cx: &mut Context<Self>) {
let (text, read_back) = cell_text(self.table.read(cx).delegate().cell(row_ix, col_ix));
let message = if read_back {
"Copied value".to_string()
} else {
"Value not read back, copied as NULL".to_string()
};
self.put_on_clipboard(text, message, cx);
}
pub fn copy_as(&mut self, what: CopyAs, cx: &mut Context<Self>) {
self.commit_editor(cx);
match what {
CopyAs::Rows(format) => {
let (snapshot, total) = self.snapshot_capped(self.row_scope(cx), MAX_COPY_ROWS, cx);
self.copy_snapshot(format, snapshot, total, cx);
}
CopyAs::ColumnValues(column) => {
let snapshot = self.snapshot(Scope::Column(column), cx);
let cells = column_cells(&snapshot);
let rendered = export::values(&cells);
let message = with_skipped(
format!("Copied {}", count_of(cells.len(), "value", "values")),
rendered.skipped,
);
self.put_on_clipboard(rendered.text, message, cx);
}
CopyAs::ColumnInList(column) => {
let snapshot = self.snapshot(Scope::Column(column), cx);
let cells = column_cells(&snapshot);
let type_name = snapshot.types.first().cloned().unwrap_or_default();
let rendered = export::in_list(self.engine(cx), &type_name, &cells);
let copied = cells
.iter()
.filter(|cell| cell.is_some() && !query::is_placeholder(cell))
.count();
let message = if copied == 0 {
"No values to copy".to_string()
} else {
with_skipped(
format!(
"Copied {} as an IN list",
count_of(copied, "value", "values")
),
rendered.skipped,
)
};
self.put_on_clipboard(rendered.text, message, cx);
}
}
}
fn copy_snapshot(
&mut self,
format: Format,
snapshot: Snapshot,
total: usize,
cx: &mut Context<Self>,
) {
let rows = snapshot.rows.len();
let engine = self.engine(cx);
let table = self.table_name(cx).unwrap_or_default();
cx.spawn(async move |this, cx| {
let rendered = cx
.background_spawn(async move {
export::render(
format,
engine,
&table,
&snapshot.columns,
&snapshot.types,
&snapshot.rows,
)
})
.await;
let (text, capped_bytes) = cap_bytes(rendered.text);
let message = if rows < total {
format!("Copied first {rows} of {total} rows as {}", format.label())
} else if capped_bytes {
format!(
"Copied the first {} MB as {}",
MAX_COPY_BYTES / (1024 * 1024),
format.label()
)
} else {
format!(
"Copied {} as {}",
count_of(rows, "row", "rows"),
format.label()
)
};
this.update_in(cx, |this, _window, cx| {
this.put_on_clipboard(text, with_skipped(message, rendered.skipped), cx);
})
.ok();
})
.detach();
}
fn put_on_clipboard(&mut self, text: String, message: String, cx: &mut Context<Self>) {
cx.write_to_clipboard(ClipboardItem::new_string(text));
cx.emit(Copied { message });
}
fn row_scope(&self, cx: &App) -> Scope {
if self.table.read(cx).delegate().rows_selected.is_empty() {
Scope::All
} else {
Scope::Picked
}
}
pub(super) fn engine(&self, cx: &App) -> Engine {
self.table.read(cx).delegate().engine
}
pub(super) fn table_name(&self, cx: &App) -> Option<String> {
self.table.read(cx).delegate().table.clone()
}
pub fn snapshot(&self, scope: Scope, cx: &App) -> Snapshot {
self.snapshot_capped(scope, usize::MAX, cx).0
}
fn snapshot_capped(&self, scope: Scope, limit: usize, cx: &App) -> (Snapshot, usize) {
let delegate = self.table.read(cx).delegate();
let mut row_ixs: Vec<usize> = match scope {
Scope::All => (0..delegate.rows()).collect(),
Scope::Picked => delegate.rows_selected.iter().copied().collect(),
Scope::Column(_) if delegate.rows_selected.is_empty() => (0..delegate.rows()).collect(),
Scope::Column(_) => delegate.rows_selected.iter().copied().collect(),
};
row_ixs.sort_unstable();
let total = row_ixs.len();
row_ixs.truncate(limit);
let (columns, types): (Vec<String>, Vec<String>) = match scope {
Scope::Column(column) => (
delegate
.result
.columns
.get(column)
.cloned()
.into_iter()
.collect(),
delegate
.result
.column_types
.get(column)
.cloned()
.into_iter()
.collect(),
),
_ => (
delegate.result.columns.clone(),
delegate.result.column_types.clone(),
),
};
let col_ixs: Vec<usize> = match scope {
Scope::Column(column) => vec![column; columns.len()],
_ => (0..columns.len()).collect(),
};
let rows = row_ixs
.iter()
.map(|row_ix| {
col_ixs
.iter()
.map(|col_ix| delegate.cell(*row_ix, *col_ix).clone())
.collect()
})
.collect();
(
Snapshot {
columns,
types,
rows,
},
total,
)
}
}
const MAX_COPY_ROWS: usize = 100_000;
const MAX_COPY_BYTES: usize = 50 * 1024 * 1024;
fn column_cells(snapshot: &Snapshot) -> Vec<Cell> {
snapshot
.rows
.iter()
.map(|row| row.first().cloned().unwrap_or(None))
.collect()
}
fn cell_text(cell: &Cell) -> (String, bool) {
if query::is_placeholder(cell) {
return (String::new(), false);
}
(cell.clone().unwrap_or_default(), true)
}
fn count_of(count: usize, one: &str, many: &str) -> String {
if count == 1 {
format!("1 {one}")
} else {
format!("{count} {many}")
}
}
fn with_skipped(mut message: String, skipped: usize) -> String {
if skipped > 0 {
message.push_str(&format!(
" ({} not read back, copied as NULL)",
count_of(skipped, "value", "values")
));
}
message
}
fn cap_bytes(mut text: String) -> (String, bool) {
if text.len() <= MAX_COPY_BYTES {
return (text, false);
}
let mut end = MAX_COPY_BYTES;
while !text.is_char_boundary(end) {
end -= 1;
}
text.truncate(end);
(text, true)
}