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:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+240
View File
@@ -0,0 +1,240 @@
use ely_gpui_component::forms::TextInput;
use ely_office::{FindReplace, Finder, prelude::*};
use gpui::{AppContext as _, Entity, TestAppContext};
use crate::docs::Docs;
use crate::tests::{blank, docs, settle, settled, text_of, type_into, with_window, written};
fn finder<R>(cx: &TestAppContext, docs: &Entity<Docs>, read: impl Fn(&Finder) -> R) -> R {
cx.update(|app| {
read(
docs.read(app)
.core
.finder
.as_ref()
.expect("the finder is open"),
)
})
}
fn total(app: &gpui::App, docs: &Entity<Docs>) -> Option<usize> {
docs.read(app)
.core
.finder
.as_ref()
.map(|finder| finder.all.len())
}
#[test]
fn find_counts_steps_replaces_and_counts_words() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "cat Cat cat dog");
settle(&cx, |app| {
docs.read(app)
.active_tab()
.and_then(|tab| tab.extra.counts)
.is_some_and(|counts| counts.words == 4)
});
with_window(&mut cx, &docs, |docs, window, cx| {
docs.on_find(&FindReplace, window, cx)
});
let query = finder(&cx, &docs, |finder| finder.query.clone());
cx.update(|app| query.update(app, |query, cx| query.set_text("cat", cx)));
settle(&cx, |app| total(app, &docs) == Some(3));
cx.update(|app| {
docs.update(app, |docs, cx| {
docs.core.finder.as_mut().expect("open").options.case = true;
docs.count_matches(cx);
})
});
settle(&cx, |app| total(app, &docs) == Some(2));
cx.update(|app| docs.update(app, |docs, cx| docs.step(true, cx)));
settle(&cx, |app| {
docs.read(app)
.core
.finder
.as_ref()
.is_some_and(|finder| finder.current == Some(0))
});
with_window(&mut cx, &docs, |docs, window, cx| {
let finder = docs.core.finder.as_mut().expect("open");
finder.replace = Some(cx.new(|cx| TextInput::new(window, cx)));
});
let with = finder(&cx, &docs, |finder| {
finder.replace.clone().expect("a field")
});
cx.update(|app| with.update(app, |with, cx| with.set_text("cow", cx)));
cx.update(|app| docs.update(app, |docs, cx| docs.replace(false, cx)));
settle(&cx, |app| total(app, &docs) == Some(1));
cx.update(|app| docs.update(app, |docs, cx| docs.replace(true, cx)));
settle(&cx, |app| total(app, &docs) == Some(0));
settled(&cx, &docs);
let path = dir.path().join("found.odt");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, path.clone(), None, window, cx)
});
written(&cx, &path, None);
let text = text_of(&mut cx, &docs, &path);
assert!(text.contains("cow Cat cow dog"), "the cased matches go");
}
#[test]
fn a_misspelled_word_offers_suggestions() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "Teh end");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.on_find(&FindReplace, window, cx)
});
let query = finder(&cx, &docs, |finder| finder.query.clone());
cx.update(|app| query.update(app, |query, cx| query.set_text("Teh", cx)));
settle(&cx, |app| total(app, &docs) == Some(1));
let (x, y, _, h) = finder(&cx, &docs, |finder| finder.all[0]);
let (worker, doc) = cx.update(|app| {
let docs = docs.read(app);
(
docs.core.worker.clone(),
docs.active_tab().expect("a tab").doc.clone(),
)
});
{
let mut worker = worker.lock().expect("the worker lock");
for (kind, buttons) in [(0, 1), (1, 1), (0, 4), (1, 4)] {
let (x, y, count, modifiers) = (x + 60, y + h / 2, 1, 0);
let event = ely_lok::Mouse {
kind,
x,
y,
count,
buttons,
modifiers,
};
worker.post_mouse(&doc, event).expect("the click posts");
}
}
settle(&cx, |app| docs.read(app).offer.is_some());
let rule = cx.update(|app| {
let offer = docs.read(app).offer.as_ref().expect("an offer");
let (_, rule) = offer
.suggestions
.iter()
.find(|(label, _)| label == "The")
.expect("The is offered");
rule.clone()
});
cx.update(|app| docs.update(app, |docs, cx| docs.run_active("spell.apply", &[&rule], cx)));
settled(&cx, &docs);
let path = dir.path().join("spelled.odt");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, path.clone(), None, window, cx)
});
written(&cx, &path, None);
assert!(
text_of(&mut cx, &docs, &path).contains("The end"),
"the suggestion lands"
);
}
#[test]
fn edits_mark_the_count_and_steps_scroll_to_far_matches() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let filler = "<text:p>filler</text:p>".repeat(100);
let body = format!(
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" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text"><office:body><office:text><text:p>a b c</text:p>{filler}<text:p>needle</text:p></office:text></office:body></office:document>"#
);
let path = dir.path().join("long.fodt");
std::fs::write(&path, body).expect("the file writes");
with_window(&mut cx, &docs, |docs, _, cx| {
docs.open_file(path.clone(), cx)
});
settle(&cx, |app| docs.read(app).core.tabs.len() == 1);
settled(&cx, &docs);
with_window(&mut cx, &docs, |docs, window, cx| {
docs.on_find(&FindReplace, window, cx)
});
let query = finder(&cx, &docs, |finder| finder.query.clone());
cx.update(|app| query.update(app, |query, cx| query.set_text(" ", cx)));
settle(&cx, |app| total(app, &docs) == Some(2));
cx.update(|app| query.update(app, |query, cx| query.set_text("needle", cx)));
settle(&cx, |app| total(app, &docs) == Some(1));
cx.update(|app| docs.update(app, |docs, cx| docs.step(true, cx)));
settle(&cx, |app| {
let tab = docs.read(app).active_tab().expect("a tab");
tab.view.read(app).scroll().1 > 20_000
});
type_into(&mut cx, &docs, "needle needle");
settle(&cx, |app| {
docs.read(app)
.core
.finder
.as_ref()
.is_some_and(|finder| finder.stale)
});
cx.update(|app| docs.update(app, |docs, cx| docs.step(true, cx)));
settle(&cx, |app| {
let finder = docs.read(app).core.finder.as_ref().expect("open");
!finder.stale && finder.all.len() == 2
});
}
#[test]
fn a_step_asked_during_a_count_waits_for_it() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
blank(&mut cx, &docs);
type_into(&mut cx, &docs, "cat cat cat");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.on_find(&FindReplace, window, cx)
});
let query = finder(&cx, &docs, |finder| finder.query.clone());
cx.update(|app| query.update(app, |query, cx| query.set_text("cat", cx)));
cx.update(|app| docs.update(app, |docs, cx| docs.step(true, cx)));
cx.update(|app| docs.update(app, |docs, cx| docs.step(true, cx)));
assert_eq!(
finder(&cx, &docs, |finder| finder.queued.len()),
2,
"both steps wait for the count"
);
settle(&cx, |app| {
docs.read(app)
.core
.finder
.as_ref()
.is_some_and(|finder| finder.all.len() == 3 && finder.current == Some(1))
});
cx.update(|app| query.update(app, |query, cx| query.set_text("ca", cx)));
cx.update(|app| query.update(app, |query, cx| query.set_text("", cx)));
settle(&cx, |app| {
docs.read(app)
.core
.finder
.as_ref()
.is_some_and(|finder| finder.pending.is_empty())
});
assert_eq!(
finder(&cx, &docs, |finder| finder.all.len()),
0,
"a cleared query voids the count on its way"
);
}
+217
View File
@@ -0,0 +1,217 @@
use gpui::TestAppContext;
use super::*;
/// The report template with an embedded Basic library.
fn macro_fodt(cx: &mut TestAppContext) -> String {
cx.update(|app| Template::Report.fodt(app))
.replace(
"office:version=",
"xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\" xmlns:ooo=\"http://openoffice.org/2004/office\" office:version=",
)
.replace(
"<office:body>",
"<office:scripts><office:script script:language=\"ooo:Basic\"><ooo:libraries><ooo:library-embedded ooo:name=\"Standard\"><ooo:module ooo:name=\"Module1\"><ooo:source-code>Sub Main&#x0A;End Sub</ooo:source-code></ooo:module></ooo:library-embedded></ooo:libraries></office:script></office:scripts><office:body>",
)
}
#[test]
fn zip_macro_checks_read_real_kernel_files() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let source = dir.path().join("basic.fodt");
std::fs::write(&source, macro_fodt(&mut cx)).expect("writes");
let worker = cx.update(|app| docs.read(app).core.worker.clone());
let mut worker = worker.lock().expect("the worker lock");
let doc = worker.load(&ely_lok::file_url(&source)).expect("loads");
let (odt, docx) = (dir.path().join("basic.odt"), dir.path().join("plain.docx"));
doc.save_as(&mut worker, &ely_lok::file_url(&odt), "odt")
.expect("odt");
doc.save_as(&mut worker, &ely_lok::file_url(&docx), "docx")
.expect("docx");
worker.close_doc(doc).expect("closes");
assert!(
ely_office::files::macro_risk(&Docs::KIND, &odt).expect("odt"),
"an ODT with Basic is a risk"
);
assert!(
!ely_office::files::macro_risk(&Docs::KIND, &docx).expect("docx"),
"a DOCX drops the Basic"
);
}
#[test]
fn a_local_template_with_macros_opens_in_risk_mode() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let template = dir.path().join("macro-template.fodt");
std::fs::write(&template, macro_fodt(&mut cx)).expect("writes");
with_window(&mut cx, &docs, |docs, _, cx| {
docs.new_from_file(&template, cx)
});
settle(&cx, |app| docs.read(app).core.active.is_some());
cx.update(|app| {
assert!(
docs.read(app).active_tab().expect("a tab").risk.is_some(),
"the copy shows the risk"
)
});
}
#[test]
fn select_all_alone_leaves_the_tab_clean() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
blank(&mut cx, &docs);
let view = cx.update(|app| docs.read(app).active_tab().expect("a tab").view.clone());
view.update(&mut cx, |view, _| {
view.run("edit.select_all", &[]).expect("select all queues")
});
settle(&cx, |app| view.read(app).settled());
for _ in 0..40 {
cx.executor().tick();
}
cx.update(|app| {
assert!(
!docs.read(app).active_tab().expect("a tab").dirty(app),
"selection is no edit"
)
});
}
#[test]
fn keep_mine_waits_and_two_queued_saves_never_crash() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
let mine = dir.path().join("mine.docx");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, mine.clone(), None, window, cx)
});
written(&cx, &mine, None);
type_into(&mut cx, &docs, "x");
std::thread::sleep(std::time::Duration::from_millis(20));
std::fs::write(&mine, "EXTERNAL").expect("the outside write");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.save(id, None, window, cx)
});
commit(&mut cx, &docs, "PendingKeepMine");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.keep_mine(id, Some(After::Tab), window, cx)
});
settle(&cx, |app| docs.read(app).core.tabs.is_empty());
let saved = text_of(&mut cx, &docs, &mine);
assert!(
saved.contains("PendingKeepMine"),
"keep mine saved the queued text: {}",
saved[saved.find("<office:text").unwrap_or(0)..].replace("\n", " ")
);
let id = blank(&mut cx, &docs);
commit(&mut cx, &docs, "queued");
let (one, two) = (dir.path().join("one.docx"), dir.path().join("two.docx"));
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, one.clone(), Some(After::Tab), window, cx)
});
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, two.clone(), None, window, cx)
});
settle(&cx, |app| docs.read(app).core.tabs.is_empty());
for _ in 0..40 {
cx.executor().tick();
}
assert!(
one.exists() && !two.exists(),
"the closed tab saves no more"
);
}
#[test]
fn a_kernel_write_failure_stays_visible() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "RetainedText");
let work = cx.update(|app| docs.read(app).active_tab().expect("a tab").work.clone());
std::fs::remove_dir_all(&work).expect("drops the scratch");
std::fs::write(&work, b"not a folder").expect("blocks it");
let target = dir.path().join("failure.docx");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, target.clone(), None, window, cx)
});
settle(&cx, |app| {
let tab = docs.read(app).active_tab().expect("a tab");
!tab.exporting && tab.session.state() == ely_files::SaveState::SaveFailed
});
assert!(!target.exists(), "the failure writes nothing");
std::fs::remove_file(&work).expect("unblocks");
std::fs::create_dir_all(&work).expect("the scratch returns");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, target.clone(), None, window, cx)
});
written(&cx, &target, None);
assert!(target.exists(), "a retry lands");
cx.update(|app| {
let state = docs.read(app).active_tab().expect("a tab").session.state();
assert_eq!(
state,
ely_files::SaveState::Saved,
"the retry clears the failure"
);
});
}
#[test]
fn an_edit_that_just_landed_still_asks_and_selection_does_not() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
commit(&mut cx, &docs, "JustLanded");
let view = cx.update(|app| docs.read(app).active_tab().expect("a tab").view.clone());
settle(&cx, |app| view.read(app).settled());
with_window(&mut cx, &docs, |docs, window, cx| {
docs.request_close(id, After::Tab, window, cx)
});
settle(&cx, |app| {
matches!(docs.read(app).core.dialog, Some(Dialog::Unsaved { .. }))
});
assert!(
cx.update(|app| docs.read(app).tab(id).is_some()),
"a landed edit asks before closing"
);
let id = blank(&mut cx, &docs);
let view = cx.update(|app| docs.read(app).active_tab().expect("a tab").view.clone());
view.update(&mut cx, |view, _| {
view.run("edit.select_all", &[]).expect("select all queues")
});
settle(&cx, |app| view.read(app).settled());
with_window(&mut cx, &docs, |docs, window, cx| {
docs.request_close(id, After::Tab, window, cx)
});
settle(&cx, |app| docs.read(app).tab(id).is_none());
}
+166
View File
@@ -0,0 +1,166 @@
use ely_office::After;
use gpui::TestAppContext;
use super::*;
use crate::merge::read_csv;
#[test]
fn typing_during_a_closing_save_is_never_lost() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "BeforeExport ");
let path = dir.path().join("closing.odt");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, path.clone(), Some(After::Tab), window, cx)
});
commit(&mut cx, &docs, "AfterExport");
settle(&cx, |app| {
let docs = docs.read(app);
docs.tab(id).is_none() || matches!(docs.core.dialog, Some(Dialog::Unsaved { .. }))
});
let closed = cx.update(|app| docs.read(app).tab(id).is_none());
let text = text_of(&mut cx, &docs, &path);
assert!(text.contains("BeforeExport"), "the export saved");
assert!(
!closed || text.contains("AfterExport"),
"a closed tab lost later typing"
);
}
#[test]
fn two_tabs_saving_as_one_path_keep_one_owner() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let first = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "FirstOwner");
let second = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "SecondOwner");
let same = dir.path().join("same.odt");
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(first, same.clone(), None, window, cx);
docs.write_as(second, same.clone(), None, window, cx);
});
written(&cx, &same, None);
settle(&cx, |app| {
docs.read(app).core.tabs.iter().all(|tab| !tab.exporting)
});
let owners: Vec<u64> = cx.update(|app| {
let docs = docs.read(app);
docs.core
.tabs
.iter()
.filter(|tab| {
tab.session
.path()
.is_some_and(|path| ely_files::same_file(path, &same).expect("comparable"))
})
.map(|tab| tab.id)
.collect()
});
assert_eq!(owners.len(), 1, "one tab holds the file");
let text = text_of(&mut cx, &docs, &same);
let mark = if owners[0] == first {
"FirstOwner"
} else {
"SecondOwner"
};
assert!(text.contains(mark), "the file is its owner's");
}
#[test]
fn interleaved_previews_keep_their_sources() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let alpha = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "SourceAlpha ");
update_doc(&mut cx, &docs, |docs, cx| docs.insert_field("Name", cx));
let beta = blank(&mut cx, &docs);
type_into(&mut cx, &docs, "SourceBeta ");
update_doc(&mut cx, &docs, |docs, cx| docs.insert_field("Name", cx));
settled(&cx, &docs);
let csv = dir.path().join("two.csv");
std::fs::write(&csv, "Name\nAda\nGrace\n").expect("the csv writes");
let table = read_csv(&csv).expect("the csv reads");
update_doc(&mut cx, &docs, |docs, cx| {
docs.preview_record(alpha, &table, 0, cx);
docs.preview_record(beta, &table, 1, cx);
});
settle(&cx, |app| {
let docs = docs.read(app);
// A filled preview is a modified copy.
docs.core.tabs.len() == 4 && docs.core.tabs[2..].iter().all(|tab| tab.modified)
});
let previews: Vec<u64> = cx.update(|app| {
let tabs = &docs.read(app).core.tabs;
tabs[2..].iter().map(|tab| tab.id).collect()
});
let mut texts = Vec::new();
for (n, id) in previews.into_iter().enumerate() {
let path = dir.path().join(format!("preview{n}.odt"));
with_window(&mut cx, &docs, |docs, window, cx| {
docs.write_as(id, path.clone(), None, window, cx)
});
written(&cx, &path, None);
texts.push(text_of(&mut cx, &docs, &path));
}
assert!(
texts.iter().any(|text| text.contains("SourceAlpha Ada")),
"{texts:?}"
);
assert!(
texts.iter().any(|text| text.contains("SourceBeta Grace")),
"{texts:?}"
);
}
#[test]
fn closing_a_clean_tab_never_waits_for_the_worker() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some(docs) = docs(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
let id = blank(&mut cx, &docs);
settled(&cx, &docs);
let worker = cx.update(|app| docs.read(app).core.worker.clone());
let (held, hold) = std::sync::mpsc::channel();
let holder = std::thread::spawn(move || {
let _busy = worker.lock().expect("the worker lock");
held.send(()).expect("the test waits");
std::thread::sleep(std::time::Duration::from_millis(400));
});
hold.recv().expect("the lock is held");
let started = std::time::Instant::now();
with_window(&mut cx, &docs, |docs, window, cx| {
docs.request_close(id, After::Tab, window, cx)
});
let took = started.elapsed();
assert!(
took < std::time::Duration::from_millis(100),
"the close waited {took:?}"
);
holder.join().expect("the holder ends");
settle(&cx, |app| docs.read(app).tab(id).is_none());
}
fn update_doc(
cx: &mut TestAppContext,
docs: &Entity<Docs>,
f: impl FnOnce(&mut Docs, &mut gpui::Context<Docs>),
) {
cx.update(|app| docs.update(app, f));
}