Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,302 @@
|
||||
use ely_gpui_component::tables::{FilterRule, SortKey, Test};
|
||||
use ely_lok::{Request, sheet};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Name, Qty, City; one repeated row.
|
||||
const DATA: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
|
||||
<office:body><office:spreadsheet><table:table table:name="S">
|
||||
<table:table-row><table:table-cell><text:p>Name</text:p></table:table-cell><table:table-cell><text:p>Qty</text:p></table:table-cell><table:table-cell><text:p>City</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>a</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="3"><text:p>3</text:p></table:table-cell><table:table-cell><text:p>x</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>b</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="1"><text:p>1</text:p></table:table-cell><table:table-cell><text:p>y</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>a</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="3"><text:p>3</text:p></table:table-cell><table:table-cell><text:p>x</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>c</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="2"><text:p>2</text:p></table:table-cell><table:table-cell><text:p>y</text:p></table:table-cell></table:table-row>
|
||||
</table:table></office:spreadsheet></office:body>
|
||||
</office:document>"#;
|
||||
|
||||
pub(super) fn open(cx: &mut TestAppContext, book: &Entity<Book>) {
|
||||
update(cx, book, |book, cx| {
|
||||
book.new_from_text("data.fods", DATA, cx)
|
||||
});
|
||||
settle(cx, |app| sheet_names(app, book) == ["S"]);
|
||||
}
|
||||
|
||||
/// Waits for the active tab's queued input.
|
||||
pub(super) fn quiet(cx: &TestAppContext, book: &Entity<Book>) {
|
||||
settle(cx, |app| {
|
||||
book.read(app)
|
||||
.active_tab()
|
||||
.is_some_and(|tab| tab.view.read(app).settled())
|
||||
});
|
||||
}
|
||||
|
||||
/// The cells of `range`, read through the worker.
|
||||
pub(super) fn cells(cx: &mut TestAppContext, book: &Entity<Book>, range: &str) -> String {
|
||||
quiet(cx, book);
|
||||
cx.update(|app| {
|
||||
let book = book.read(app);
|
||||
let tab = book.active_tab().expect("a tab");
|
||||
let mut worker = book.core.worker.lock().expect("the worker lock");
|
||||
worker
|
||||
.run(&tab.doc, "cell.goto", &[range])
|
||||
.expect("the range");
|
||||
tab.doc
|
||||
.selection_text(&mut worker)
|
||||
.expect("the text")
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn run(cx: &mut TestAppContext, book: &Entity<Book>, id: &str, values: &[&str]) {
|
||||
update(cx, book, |book, cx| book.run_active(id, values, cx));
|
||||
quiet(cx, book);
|
||||
}
|
||||
|
||||
/// The column's entries the filter drops.
|
||||
fn dropped(cx: &mut TestAppContext, book: &Entity<Book>, column: u32) -> Vec<String> {
|
||||
quiet(cx, book);
|
||||
cx.update(|app| {
|
||||
let book = book.read(app);
|
||||
let tab = book.active_tab().expect("a tab");
|
||||
let mut worker = book.core.worker.lock().expect("the worker lock");
|
||||
let json = worker
|
||||
.values(&tab.doc, &sheet::cell_query(sheet::FILTER, column, 0))
|
||||
.expect("the entries");
|
||||
sheet::filter_values(&json)
|
||||
.expect("parses")
|
||||
.entries
|
||||
.into_iter()
|
||||
.filter(|entry| !entry.selected)
|
||||
.map(|entry| entry.text)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn asked(cx: &TestAppContext, book: &Entity<Book>, is: fn(&Dialog) -> bool) {
|
||||
settle(cx, |app| book.read(app).dialog.as_ref().is_some_and(is));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_filter_duplicates_and_columns_run_from_their_dialogs() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some((_window, book)) = book(&mut cx, dir.path()) else {
|
||||
eprintln!("SKIP: no LOK build");
|
||||
return;
|
||||
};
|
||||
open(&mut cx, &book);
|
||||
run(&mut cx, &book, "cell.goto", &["B3"]);
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
book.ask_data(crate::data::Asked::Sort, cx)
|
||||
});
|
||||
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Sort { .. }));
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
let Some(Dialog::Sort {
|
||||
picked,
|
||||
keys,
|
||||
header,
|
||||
..
|
||||
}) = &mut book.dialog
|
||||
else {
|
||||
unreachable!("checked above");
|
||||
};
|
||||
assert_eq!(picked.top, ["Name", "Qty", "City"]);
|
||||
assert!(*header, "the kernel sees headers");
|
||||
assert_eq!(crate::data::title(picked, 1, *header), "Qty");
|
||||
*keys = vec![SortKey {
|
||||
column: "1".into(),
|
||||
rising: true,
|
||||
}];
|
||||
book.data_asked(cx);
|
||||
});
|
||||
assert_eq!(cells(&mut cx, &book, "A2:A5"), "b\nc\na\na");
|
||||
|
||||
run(&mut cx, &book, "cell.goto", &["A1"]);
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
book.ask_data(crate::data::Asked::Filter, cx)
|
||||
});
|
||||
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Filter { .. }));
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
if let Some(Dialog::Filter { rules, .. }) = &mut book.dialog {
|
||||
*rules = vec![FilterRule {
|
||||
column: "2".into(),
|
||||
test: Test::Equals,
|
||||
value: "x".into(),
|
||||
}];
|
||||
}
|
||||
book.data_asked(cx);
|
||||
});
|
||||
assert_eq!(dropped(&mut cx, &book, 2), ["y"]);
|
||||
run(&mut cx, &book, "data.unfilter", &[]);
|
||||
|
||||
run(&mut cx, &book, "cell.goto", &["A2"]);
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
book.ask_data(crate::data::Asked::Dedupe, cx)
|
||||
});
|
||||
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Dedupe { .. }));
|
||||
update(&mut cx, &book, |book, cx| book.data_asked(cx));
|
||||
assert_eq!(cells(&mut cx, &book, "A1:A5"), "Name\nb\nc\na");
|
||||
|
||||
run(&mut cx, &book, "cell.goto", &["A7"]);
|
||||
run(&mut cx, &book, "cell.enter", &["p;q;r"]);
|
||||
run(&mut cx, &book, "cell.goto", &["A7"]);
|
||||
update(&mut cx, &book, |book, cx| book.ask_columns(cx));
|
||||
asked(&cx, &book, |dialog| {
|
||||
matches!(dialog, Dialog::Columns { .. })
|
||||
});
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
if let Some(Dialog::Columns { separators, .. }) = &mut book.dialog {
|
||||
*separators = vec!["59".into()];
|
||||
}
|
||||
book.columns_asked(cx);
|
||||
});
|
||||
assert_eq!(cells(&mut cx, &book, "A7:C7"), "p\tq\tr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_column_button_lists_validation_offers_and_formats_list() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some((window, book)) = book(&mut cx, dir.path()) else {
|
||||
eprintln!("SKIP: no LOK build");
|
||||
return;
|
||||
};
|
||||
open(&mut cx, &book);
|
||||
run(&mut cx, &book, "cell.goto", &["A1"]);
|
||||
run(&mut cx, &book, "data.autofilter", &[]);
|
||||
let id = cx.update(|app| book.read(app).core.active.expect("a tab"));
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
book.ask_column(id, &Request::Filter { column: 0, row: 0 }, cx)
|
||||
});
|
||||
asked(&cx, &book, |dialog| matches!(dialog, Dialog::Column { .. }));
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
let Some(Dialog::Column {
|
||||
entries, picked, ..
|
||||
}) = &mut book.dialog
|
||||
else {
|
||||
unreachable!("checked above");
|
||||
};
|
||||
assert_eq!(picked.len(), entries.len(), "nothing filtered yet");
|
||||
*picked = vec!["c".into()];
|
||||
book.column_asked(None, cx);
|
||||
});
|
||||
assert_eq!(dropped(&mut cx, &book, 0), ["a", "b"]);
|
||||
run(&mut cx, &book, "data.unfilter", &[]);
|
||||
|
||||
run(&mut cx, &book, "cell.goto", &["E2"]);
|
||||
update(&mut cx, &book, |book, cx| book.ask_rule(cx));
|
||||
asked(&cx, &book, |dialog| {
|
||||
matches!(dialog, Dialog::Validation { .. })
|
||||
});
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
let Some(Dialog::Validation { rule, fields, .. }) = &mut book.dialog else {
|
||||
unreachable!("checked above");
|
||||
};
|
||||
rule.kind = "list".into();
|
||||
rule.show_input = true;
|
||||
fields
|
||||
.list
|
||||
.update(cx, |list, cx| list.set_text("Red\nGreen", cx));
|
||||
fields
|
||||
.input_message
|
||||
.update(cx, |note, cx| note.set_text("Pick a color", cx));
|
||||
book.rule_asked(cx);
|
||||
});
|
||||
run(&mut cx, &book, "cell.goto", &["E3"]);
|
||||
run(&mut cx, &book, "cell.goto", &["E2"]);
|
||||
settle(&cx, |app| {
|
||||
book.read(app)
|
||||
.active_tab()
|
||||
.and_then(|tab| tab.extra.rule.as_ref())
|
||||
.is_some_and(|(at, rule)| at.1 == "E2" && rule.input_message == "Pick a color")
|
||||
});
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
book.offer_choices(id, (4, 1), cx)
|
||||
});
|
||||
settle(&cx, |app| book.read(app).choices.is_some());
|
||||
let values = cx.update(|app| {
|
||||
let choices = book.read(app).choices.as_ref().expect("the menu");
|
||||
choices.values.clone()
|
||||
});
|
||||
assert_eq!(values, ["Red", "Green"]);
|
||||
|
||||
run(&mut cx, &book, "cell.goto", &["B2:B5"]);
|
||||
cx.update_window(window.into(), |_, window, app| {
|
||||
book.update(app, |book, cx| book.ask_conditional(window, cx))
|
||||
})
|
||||
.expect("the window lives");
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
if let Some(Dialog::Conditional { value1, .. }) = &book.dialog {
|
||||
value1.update(cx, |value, cx| value.set_text("2", cx));
|
||||
}
|
||||
book.conditional_asked(cx);
|
||||
});
|
||||
quiet(&cx, &book);
|
||||
update(&mut cx, &book, |book, cx| book.ask_formats(cx));
|
||||
asked(&cx, &book, |dialog| {
|
||||
matches!(dialog, Dialog::Formats { .. })
|
||||
});
|
||||
let style = cx.update(|app| match &book.read(app).dialog {
|
||||
Some(Dialog::Formats { formats, .. }) => formats[0].style.clone(),
|
||||
_ => unreachable!("checked above"),
|
||||
});
|
||||
assert_eq!(style, "Good");
|
||||
update(&mut cx, &book, |book, cx| book.format_removed(cx));
|
||||
quiet(&cx, &book);
|
||||
update(&mut cx, &book, |book, cx| book.ask_formats(cx));
|
||||
assert!(
|
||||
cx.update(|app| book.read(app).dialog.is_none()),
|
||||
"none left"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_reads_number_formats_and_find_counts_cells() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some((window, book)) = book(&mut cx, dir.path()) else {
|
||||
eprintln!("SKIP: no LOK build");
|
||||
return;
|
||||
};
|
||||
open(&mut cx, &book);
|
||||
run(&mut cx, &book, "cell.goto", &["B2"]);
|
||||
run(&mut cx, &book, "number.percent", &[]);
|
||||
update(&mut cx, &book, |book, cx| {
|
||||
let tab = book.active_tab().expect("a tab");
|
||||
let shown = (tab.view.clone(), tab.doc.clone());
|
||||
book.panel
|
||||
.update(cx, |panel, cx| panel.show(Some((&shown.0, &shown.1)), cx));
|
||||
});
|
||||
let kind = |app: &gpui::App| book.read(app).panel.read(app).number.clone();
|
||||
settle(&cx, |app| {
|
||||
kind(app).is_some_and(|(_, kind)| kind == "percent")
|
||||
});
|
||||
let panel = cx.update(|app| book.read(app).panel.clone());
|
||||
cx.update(|app| panel.update(app, |panel, cx| panel.apply_code("[ZZZ]0", cx)));
|
||||
quiet(&cx, &book);
|
||||
cx.update(|app| panel.update(app, |panel, cx| panel.apply_code("0.0", cx)));
|
||||
settle(&cx, |app| kind(app).is_some_and(|(code, _)| code == "0.0"));
|
||||
|
||||
cx.update_window(window.into(), |_, window, app| {
|
||||
book.update(app, |book, cx| {
|
||||
book.on_find(&ely_office::FindReplace, window, cx)
|
||||
})
|
||||
})
|
||||
.expect("the window lives");
|
||||
let query = cx.update(|app| {
|
||||
let finder = book.read(app).core.finder.as_ref().expect("the finder");
|
||||
finder.query.clone()
|
||||
});
|
||||
cx.update(|app| query.update(app, |query, cx| query.set_text("a", cx)));
|
||||
settle(&cx, |app| {
|
||||
book.read(app)
|
||||
.core
|
||||
.finder
|
||||
.as_ref()
|
||||
.is_some_and(|finder| finder.all.len() == 3)
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user