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,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"
|
||||
);
|
||||
}
|
||||
@@ -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
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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user