Files
ElyOffice/apps/ely-doc/src/tests/find.rs
T
ZacharyZhang-NYandClaude Opus 5.5 87886c8a8d R5.5: ElyTable right-click menus, Excel-style; three rounds used, last fixes tested
Grid and header menus share ely_office::menu with ElyDoc. Each item's
availability is the kernel's state for the exact command it runs
(patch 0045). Patch 0046 lets a header select its row or column while
a shape is selected or its text is edited: the view shell ends the
drawing edit, keeps the typed text, leaves drawing mode, then marks.
Ely no longer sends Esc first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 13:02:10 -04:00

242 lines
8.8 KiB
Rust

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 spelling = offer.what.spelling.as_ref().expect("a spelling menu");
let (_, rule) = spelling
.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"
);
}