R9.5: merge PR #6, ElyTable completion keys and overlay clicks
Enter and Tab are resolved by the worker against the kernel's current candidates, after the typing queued before them. The dropdown arrow's mouse blocker is at least as tall as the button.
This commit is contained in:
22 files changed
+696
-48
No files matched your search
@@ -325,3 +325,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
|
||||
- 2026-10-19(用户定):自有代码由 AGPL-3.0-or-later 改为 MIT(`LICENSE`,Copyright (c) 2026 Yang Zhang;工作区 `Cargo.toml`、Linux 包的许可字段、`docs/licenses.md`、官网)。第三方照旧:LibreOffice 与 `patches/libreoffice` 为 MPL-2.0 等,Git 为 GPL-2.0,MuPDF 为 AGPL-3.0;ElyPDF 链接 MuPDF,其发行包整体仍须按 AGPL 履行,Linux 包的许可字段据此写为 `MIT AND <引擎许可>`。
|
||||
- 2026-10-19(GitHub #2,R9.1):Impress 在母版视图里 `getParts` 数母版、`getPartName` 给母版名,`getPartInfo` 却读同一序号的幻灯片,母版多于幻灯片时信息里没有 `visible`,ElyShow 每次重读部件都报错;补丁 `0052-lok-impress-master-view-part-info.patch` 让母版视图里取母版页(`client/tests/show.rs` 核对)。`ELY_SHARE` 下发布流程不重建内核:新补丁须先在三台机器的共享引擎上各自重建。GitHub PR #1(R9.2,用户定合并):选区画在图块之上,不透明的选区色盖住文字,`LokView` 画选区时不透明度至多 0.3。
|
||||
- 2026-10-20(GitHub #4,R9.3):整列看不出标题时 Calc 开自动筛选会弹异步询问,worker 当场关掉即在其回调里崩溃;补丁 `0053-lok-calc-autofilter-header-without-asking.patch`:LOK 下不问,首行作标题(同 Excel)。R9.4:arboard 在 Windows 把剪贴板上没有 HTML 读成错误,`ely_platform::clipboard` 先查格式;剪贴板错误的提示写出整条原因。补丁 0049 补上 `doc.hxx`(Linux 的 gcc 编不过;MSVC 与 clang 经别的头文件带入)。Gitea 上另发同名的过渡发布(用户定):0.1.8 及更早的应用在 Gitea 查更新,过渡发布的清单指向 GitHub 的下载,装上的 0.1.9 起从 GitHub 查;Gitea 只为此推上 main 与标签,不再是发布之处。
|
||||
- 2026-10-20(GitHub PR #6,R9.5,用户定合并):ElyTable 的函数候选由 Tab、不带修饰的 Enter 或单击接受(原 Enter 提交半个公式并下移);接受后到对应输入与其修订的事件批次到达前不再提供候选,不能重复接受(`view/formula/completion.rs`);单击按所点的函数名接受。接受与否由 worker 在执行时按内核此刻的候选判定(`worker/complete.rs`,同换页的 `TURN`):界面上回报的候选可能已被其后排队的键入作废;所选的仍在即取它,按键时不在即取菜单首项(名最短者,同菜单的次序),没有候选即照常交按键,单击的已不在即什么也不做。有效性下拉按钮挡鼠标的框至少为按钮的主题高度(默认行比按钮矮)。候选、参数提示、有效性下拉按钮与输入说明挡住滚轮以外的鼠标,按下不落到其下的格;填充与行列拖动在其上松开即取消。PR 的 ElyTable 测试原设空的错误样式,补丁 0017 拒绝该规则,改为 `stop`;空的单格读不出选区文字,改读 E2:F2。
|
||||
@@ -65,6 +65,11 @@ impl Book {
|
||||
.bg(sunken)
|
||||
.on_mouse_move(cx.listener(Self::on_drag_move))
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_drag_end))
|
||||
.on_mouse_up_out(MouseButton::Left, cx.listener(|book, _, _, cx| {
|
||||
if book.drag.take().is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(div().flex().child(corner).child(column_strip))
|
||||
.child(
|
||||
div().flex_1().min_h_0().flex().child(row_strip).child(
|
||||
|
||||
@@ -9,8 +9,8 @@ use ely_gpui_component::{
|
||||
use ely_lok::{LokView, sheet};
|
||||
use ely_office::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, Context, Entity, IntoElement, ParentElement as _, Pixels, Point, Styled as _, div,
|
||||
px,
|
||||
AnyElement, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Pixels,
|
||||
Point, Styled as _, div, px,
|
||||
};
|
||||
|
||||
use crate::book::Book;
|
||||
@@ -112,6 +112,7 @@ impl Book {
|
||||
.absolute()
|
||||
.left(corner.x)
|
||||
.top(corner.y)
|
||||
.block_mouse_except_scroll()
|
||||
.child(
|
||||
IconButton::new("table-choices-open", IconName::ChevronDown)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
@@ -121,7 +122,8 @@ impl Book {
|
||||
book.offer_choices(id, (column, row), cx)
|
||||
})),
|
||||
)
|
||||
.h(px(size.max(16.0)))
|
||||
.h(px(size))
|
||||
.min_h(cx.theme().control_height(ControlSize::Sm))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
@@ -137,6 +139,7 @@ impl Book {
|
||||
.absolute()
|
||||
.left(below.x)
|
||||
.top(below.y)
|
||||
.block_mouse_except_scroll()
|
||||
.max_w(theme.tooltip_max_width())
|
||||
.px_2()
|
||||
.py_1()
|
||||
|
||||
@@ -394,6 +394,7 @@ fn a_delete_stays_with_its_workbook() {
|
||||
}
|
||||
|
||||
mod analysis;
|
||||
mod aids;
|
||||
mod data;
|
||||
mod menu;
|
||||
mod names;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
use super::data::{cells, open, run};
|
||||
use super::*;
|
||||
use ely_gpui_component::theme::{ActiveTheme as _, ControlSize};
|
||||
use ely_lok::sheet::Validation;
|
||||
use gpui::{Modifiers, MouseButton, ParentElement as _, Styled as _, VisualTestContext, point, px};
|
||||
|
||||
struct Grid(Entity<Book>);
|
||||
|
||||
impl gpui::Render for Grid {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let grid = self.0.update(cx, |book, cx| book.grid(window, cx));
|
||||
gpui::div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(grid)
|
||||
}
|
||||
}
|
||||
|
||||
fn frames(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.update(|window, _| window.refresh());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_help_and_arrow_do_not_select_the_cells_behind_them() {
|
||||
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;
|
||||
};
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
open(&mut cx, &book);
|
||||
run(&mut cx, &book, "cell.goto", &["E2"]);
|
||||
let rule = Validation {
|
||||
kind: "list".into(),
|
||||
operator: "equal".into(),
|
||||
list: vec!["Red".into(), "Green".into()],
|
||||
dropdown: true,
|
||||
show_input: true,
|
||||
input_message: "Pick a color".into(),
|
||||
error_style: "stop".into(),
|
||||
..Validation::default()
|
||||
};
|
||||
run(&mut cx, &book, "rules.validation", &[&rule.to_json()]);
|
||||
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")
|
||||
});
|
||||
let window = cx.add_window(|_, _| Grid(book.clone()));
|
||||
let mut shown = VisualTestContext::from_window(window.into(), &cx);
|
||||
frames(&mut shown);
|
||||
let (view, help, arrow, handle) = shown.update(|window, app| {
|
||||
let view = book.read(app).active_tab().expect("a tab").view.clone();
|
||||
let read = view.read(app);
|
||||
let (x, y, w, h) = read.cell().cursor.expect("the cell cursor");
|
||||
let help = read.origin() + read.place((x, y + h)) + point(px(8.0), px(8.0));
|
||||
// The arrow's lower corner hangs below a default row.
|
||||
let tall = app
|
||||
.theme()
|
||||
.control_height(ControlSize::Sm)
|
||||
.to_pixels(window.rem_size());
|
||||
let corner = point(tall - px(1.0), tall - px(1.0));
|
||||
let arrow = read.origin() + read.place((x + w, y)) + corner;
|
||||
let handle = read.origin() + read.place((x + w, y + h)) - point(px(1.0), px(1.0));
|
||||
(view.clone(), help, arrow, handle)
|
||||
});
|
||||
let sent = shown.update(|_, app| view.read(app).sent());
|
||||
shown.simulate_mouse_down(handle, MouseButton::Left, Modifiers::none());
|
||||
assert!(
|
||||
shown.update(|_, app| matches!(book.read(app).drag, Some(crate::grid::Drag::Fill { .. })))
|
||||
);
|
||||
shown.simulate_mouse_move(help, MouseButton::Left, Modifiers::none());
|
||||
shown.simulate_mouse_up(help, MouseButton::Left, Modifiers::none());
|
||||
assert!(
|
||||
shown.update(|_, app| book.read(app).drag.is_none()),
|
||||
"releasing over help must end the drag"
|
||||
);
|
||||
shown.simulate_click(help, Modifiers::none());
|
||||
assert_eq!(
|
||||
shown.update(|_, app| view.read(app).sent()),
|
||||
sent,
|
||||
"help never sends a grid click"
|
||||
);
|
||||
shown.simulate_mouse_down(arrow, MouseButton::Left, Modifiers::none());
|
||||
assert_eq!(
|
||||
shown.update(|_, app| view.read(app).sent()),
|
||||
sent,
|
||||
"the arrow press stays outside the grid"
|
||||
);
|
||||
shown.simulate_mouse_up(arrow, MouseButton::Left, Modifiers::none());
|
||||
settle(&shown, |app| book.read(app).choices.is_some());
|
||||
frames(&mut shown);
|
||||
shown.update(|_, app| {
|
||||
assert_eq!(view.read(app).cell().address, "E2");
|
||||
assert_eq!(
|
||||
view.read(app).sent(),
|
||||
sent,
|
||||
"the arrow release also stays outside the grid"
|
||||
);
|
||||
assert_eq!(
|
||||
book.read(app).choices.as_ref().expect("the menu").values,
|
||||
["Red", "Green"]
|
||||
);
|
||||
});
|
||||
let red = shown
|
||||
.debug_bounds("menu-row-Red")
|
||||
.expect("the Red menu item");
|
||||
shown.simulate_click(red.center(), Modifiers::none());
|
||||
// An empty F2 adds nothing after E2's tab.
|
||||
assert_eq!(
|
||||
cells(&mut cx, &book, "E2:F2"),
|
||||
"Red",
|
||||
"the value lands in E2 and F2 stays empty"
|
||||
);
|
||||
}
|
||||
@@ -76,12 +76,26 @@ impl Doc {
|
||||
Unpack::new(&payload).str()
|
||||
}
|
||||
|
||||
/// Completes the function name being typed (TABLE-03).
|
||||
pub fn complete_function(&self, worker: &mut Worker, name: &str) -> io::Result<()> {
|
||||
log::info!("lok: completes a function");
|
||||
let request = Pack::new().u64(worker.doc_id(self)?).str(name).bytes();
|
||||
worker.request(tag::COMPLETE_FUNCTION, &request)?;
|
||||
Ok(())
|
||||
/// Completes `name` if offered; a key takes any, else goes in (TABLE-03).
|
||||
pub fn complete_function(
|
||||
&self,
|
||||
worker: &mut Worker,
|
||||
name: Option<&str>,
|
||||
key: Option<(u32, i32)>,
|
||||
) -> io::Result<bool> {
|
||||
let pack = Pack::new().u64(worker.doc_id(self)?);
|
||||
let pack = match name {
|
||||
Some(name) => pack.u64(1).str(name),
|
||||
None => pack.u64(0),
|
||||
};
|
||||
// Key code 0: no key, as for a click.
|
||||
let (char_code, key_code) = key.unwrap_or((0, 0));
|
||||
let request = pack
|
||||
.i64(i64::from(char_code))
|
||||
.i64(i64::from(key_code))
|
||||
.bytes();
|
||||
let reply = worker.request(tag::COMPLETE_FUNCTION, &request)?;
|
||||
Ok(Unpack::new(&reply).u64()? == 1)
|
||||
}
|
||||
|
||||
/// Shows sheet `part`, from 0.
|
||||
|
||||
@@ -50,8 +50,8 @@ fn typing_a_formula_offers_functions_usage_and_marks() {
|
||||
sum.is_some_and(|f| f.signature.contains(", ")),
|
||||
"the signature takes the Excel separator: {sum:?}"
|
||||
);
|
||||
doc.complete_function(&mut worker, "SUMIF")
|
||||
.expect("the completion");
|
||||
let taken = doc.complete_function(&mut worker, Some("SUMIF"), None);
|
||||
assert!(taken.expect("the completion"), "SUMIF is on offer");
|
||||
let done = poll_until(&mut worker, &doc, &offered, SETTLE, |seen| {
|
||||
seen.cell.formula == "=SUMIF()"
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::ffi::Document;
|
||||
|
||||
mod cell;
|
||||
mod parse;
|
||||
pub use cell::{Cell, Function, Mark, Request};
|
||||
pub use cell::{Cell, Function, Mark, Request, offered};
|
||||
use cell::{functions_of, marks_of, request_of, usage_of};
|
||||
pub use parse::A11y;
|
||||
use parse::{
|
||||
|
||||
@@ -5,6 +5,19 @@ use serde_json::Value;
|
||||
use super::Rect;
|
||||
use crate::ipc::{Pack, Unpack};
|
||||
|
||||
/// The function names `id` offers now.
|
||||
pub fn offered(id: u64) -> Vec<String> {
|
||||
let slots = super::slots().lock().expect("the events lock");
|
||||
slots.get(&id).map_or_else(Vec::new, |slot| {
|
||||
slot.batch
|
||||
.cell
|
||||
.functions
|
||||
.iter()
|
||||
.map(|function| function.name.clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// What a spreadsheet reports about the current cell.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Cell {
|
||||
|
||||
@@ -123,6 +123,7 @@ pub struct LokView {
|
||||
pointer: gpui::CursorStyle,
|
||||
selection: Vec<Rect>,
|
||||
cell: crate::Cell,
|
||||
completion: formula::Completion,
|
||||
/// Whether the kernel was told Ely's scheme.
|
||||
lit: bool,
|
||||
/// The zoom follows the viewport so a page fits.
|
||||
@@ -218,6 +219,7 @@ impl LokView {
|
||||
pointer: gpui::CursorStyle::Arrow,
|
||||
selection: Vec::new(),
|
||||
cell: crate::Cell::default(),
|
||||
completion: formula::Completion::default(),
|
||||
lit: false,
|
||||
fit: false,
|
||||
turning: None,
|
||||
@@ -392,6 +394,7 @@ impl LokView {
|
||||
self.picked = 0;
|
||||
}
|
||||
self.cell = batch.cell;
|
||||
self.completion.reported(batch.revision);
|
||||
self.graphic = batch.graphic;
|
||||
self.read_borders(batch.table, batch.revision);
|
||||
self.review = batch.review;
|
||||
|
||||
@@ -5,11 +5,17 @@ use ely_gpui_component::{
|
||||
primitives::raise,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, Bounds, Context, Hsla, IntoElement, Keystroke, Modifiers, ParentElement, Pixels,
|
||||
anchored, rgb,
|
||||
AnyElement, Bounds, Context, Hsla, InteractiveElement, IntoElement, Keystroke, Modifiers,
|
||||
ParentElement, Pixels, anchored, div, rgb,
|
||||
};
|
||||
|
||||
use super::{LokView, input::Input};
|
||||
use super::{
|
||||
LokView,
|
||||
input::{Input, vcl_key},
|
||||
};
|
||||
|
||||
mod completion;
|
||||
pub(super) use completion::Completion;
|
||||
|
||||
impl LokView {
|
||||
/// The sheet shown, which reference marks follow.
|
||||
@@ -48,7 +54,7 @@ impl LokView {
|
||||
/// Up and Down walk the offered functions.
|
||||
pub(super) fn walk(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) -> bool {
|
||||
let count = self.cell.functions.len();
|
||||
if count == 0 || keystroke.modifiers != Modifiers::none() {
|
||||
if count == 0 || self.completion.pending() || keystroke.modifiers != Modifiers::none() {
|
||||
return false;
|
||||
}
|
||||
self.picked = match keystroke.key.as_str() {
|
||||
@@ -60,32 +66,46 @@ impl LokView {
|
||||
true
|
||||
}
|
||||
|
||||
/// Takes the chosen function, in order with typing.
|
||||
pub(super) fn complete(&mut self) -> bool {
|
||||
let Some(item) = self.offered().into_iter().nth(self.picked) else {
|
||||
return false;
|
||||
};
|
||||
log::info!("lok: completes {}", item.label);
|
||||
self.send(Input::Complete(item.label.to_string()));
|
||||
true
|
||||
/// Enter or Tab: the kernel's function then, else the key.
|
||||
pub(super) fn complete_or_press(&mut self, keystroke: &Keystroke) {
|
||||
let key = vcl_key(keystroke).expect("Enter and Tab are vcl keys");
|
||||
let name = (!self.completion.pending())
|
||||
.then(|| self.offered().into_iter().nth(self.picked))
|
||||
.flatten()
|
||||
.map(|item| item.label.to_string());
|
||||
self.take(name, Some(key));
|
||||
}
|
||||
|
||||
/// A clicked function, if the kernel still offers it then.
|
||||
fn complete_function(&mut self, name: &str) {
|
||||
if !self.completion.pending() {
|
||||
self.take(Some(name.to_string()), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues the choice in order with typing; the worker decides.
|
||||
fn take(&mut self, name: Option<String>, key: Option<(u32, i32)>) {
|
||||
self.send(Input::Complete { name, key });
|
||||
self.completion.queued(self.sent);
|
||||
self.picked = 0;
|
||||
}
|
||||
|
||||
/// The offered functions, else the call's usage, under the cell.
|
||||
pub(super) fn hints(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
if self.completion.pending() {
|
||||
return None;
|
||||
}
|
||||
let cell = self.screen_rect(self.cell.cursor?);
|
||||
let hint = if self.cell.functions.is_empty() {
|
||||
SignatureHelp::new("lok-view-usage", [usage(&self.cell.usage)?]).into_any_element()
|
||||
} else {
|
||||
let this = cx.entity();
|
||||
let offered = self.offered();
|
||||
let labels: Vec<_> = offered.iter().map(|item| item.label.clone()).collect();
|
||||
CompletionMenu::new("lok-view-functions", offered)
|
||||
.selected(self.picked)
|
||||
.on_pick(move |item, _, cx| {
|
||||
let at = labels.iter().position(|label| *label == item.label);
|
||||
this.update(cx, |view, cx| {
|
||||
view.picked = at.expect("a picked item is offered");
|
||||
view.complete();
|
||||
view.complete_function(&item.label);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -94,7 +114,7 @@ impl LokView {
|
||||
let place = anchored()
|
||||
.position(cell.bottom_left())
|
||||
.snap_to_window()
|
||||
.child(hint);
|
||||
.child(div().block_mouse_except_scroll().child(hint));
|
||||
Some(raise("lok-view-hints", place).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Completion {
|
||||
pending: Option<(u64, Option<u64>)>,
|
||||
reported: u64,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
pub(crate) fn pending(&self) -> bool {
|
||||
self.pending.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn queued(&mut self, seq: u64) {
|
||||
self.pending = Some((seq, None));
|
||||
}
|
||||
|
||||
pub(crate) fn reported(&mut self, revision: u64) {
|
||||
self.reported = revision;
|
||||
self.finish();
|
||||
}
|
||||
|
||||
pub(crate) fn landed(&mut self, seq: u64, revision: Option<u64>) {
|
||||
if self.pending.is_some_and(|(waiting, _)| waiting == seq) {
|
||||
self.pending = revision.map(|revision| (seq, Some(revision)));
|
||||
self.finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
// Inline callbacks and serialized output preserve completion revisions.
|
||||
if self
|
||||
.pending
|
||||
.is_some_and(|(_, revision)| revision.is_some_and(|revision| self.reported >= revision))
|
||||
{
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Completion;
|
||||
|
||||
#[test]
|
||||
fn an_old_snapshot_cannot_reopen_an_accepted_menu() {
|
||||
let mut completion = Completion::default();
|
||||
completion.reported(10);
|
||||
completion.queued(7);
|
||||
completion.reported(10);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(11));
|
||||
assert!(completion.pending());
|
||||
completion.reported(10);
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snapshot_before_the_reply_waits_for_the_reply() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.reported(11);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(11));
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_before_the_snapshot_waits_for_the_snapshot() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.landed(7, Some(11));
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_input_cannot_acknowledge_the_completion() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.reported(20);
|
||||
completion.landed(6, Some(20));
|
||||
assert!(completion.pending());
|
||||
completion.landed(8, None);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(19));
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_completion_allows_retry() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.landed(7, None);
|
||||
assert!(!completion.pending());
|
||||
completion.queued(8);
|
||||
assert!(completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_next_completion_requires_its_own_snapshot() {
|
||||
let mut completion = Completion::default();
|
||||
completion.reported(11);
|
||||
completion.queued(7);
|
||||
completion.landed(7, Some(11));
|
||||
assert!(!completion.pending());
|
||||
completion.queued(8);
|
||||
completion.landed(8, Some(12));
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(completion.pending());
|
||||
completion.reported(12);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,11 @@ pub(super) enum Input {
|
||||
Paste,
|
||||
/// Values or formulas alone: `paste.values` or `paste.formulas`.
|
||||
PasteOnly(&'static str),
|
||||
/// A function the kernel offered, taken.
|
||||
Complete(String),
|
||||
/// A function the kernel offers then, else Enter or Tab.
|
||||
Complete {
|
||||
name: Option<String>,
|
||||
key: Option<(u32, i32)>,
|
||||
},
|
||||
/// The kernel's color scheme; not an edit.
|
||||
Theme(&'static str),
|
||||
/// Commands in order, from one read.
|
||||
@@ -72,6 +75,8 @@ pub(super) enum Input {
|
||||
pub(super) enum Back {
|
||||
Copied(String),
|
||||
Shown(usize),
|
||||
/// Whether a function was taken.
|
||||
Completed(bool),
|
||||
}
|
||||
|
||||
impl Input {
|
||||
@@ -92,7 +97,7 @@ impl Input {
|
||||
| Input::Paste
|
||||
| Input::PasteOnly(_)
|
||||
| Input::Drag { .. }
|
||||
| Input::Complete(_) => true,
|
||||
| Input::Complete { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +206,10 @@ fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result<Option<Back
|
||||
Input::Drag { from, to } => worker.drag_graphic(doc, from, to),
|
||||
Input::Visible(area, scale) => doc.visible_area(worker, area, scale),
|
||||
Input::Part(part) => doc.show_sheet(worker, part),
|
||||
Input::Complete(name) => doc.complete_function(worker, &name),
|
||||
Input::Complete { name, key } => {
|
||||
let taken = doc.complete_function(worker, name.as_deref(), key)?;
|
||||
return Ok(Some(Back::Completed(taken)));
|
||||
}
|
||||
Input::Theme(scheme) => worker.run(doc, "view.theme", &[scheme]),
|
||||
Input::Text { commit, text } => worker.post_text(doc, commit, &text),
|
||||
Input::Copy { cut } => {
|
||||
@@ -270,6 +278,10 @@ impl LokView {
|
||||
|
||||
/// Posts a non-text key; text waits for the IME.
|
||||
pub(super) fn key_down(&mut self, keystroke: &Keystroke) -> bool {
|
||||
if keystroke.key == "enter" && keystroke.modifiers == Modifiers::none() {
|
||||
self.complete_or_press(keystroke);
|
||||
return true;
|
||||
}
|
||||
let Some((char_code, key_code)) = vcl_key(keystroke) else {
|
||||
return false;
|
||||
};
|
||||
@@ -307,10 +319,14 @@ impl LokView {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let completing = self.completion.pending();
|
||||
if self.walk(&event.keystroke, cx)
|
||||
|| self.turn_key(&event.keystroke)
|
||||
|| self.key_down(&event.keystroke)
|
||||
{
|
||||
if !completing && self.completion.pending() {
|
||||
cx.notify();
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -425,10 +441,14 @@ impl LokView {
|
||||
self.send(Input::Paste);
|
||||
}
|
||||
|
||||
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, _: &mut Context<Self>) {
|
||||
if !self.complete() {
|
||||
self.key_down(&Keystroke::parse("tab").expect("a keystroke"));
|
||||
}
|
||||
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.next_field();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Tab takes the offered function, else moves on.
|
||||
pub(super) fn next_field(&mut self) {
|
||||
self.complete_or_press(&Keystroke::parse("tab").expect("a keystroke"));
|
||||
}
|
||||
|
||||
pub(super) fn on_previous_field(
|
||||
|
||||
@@ -107,6 +107,9 @@ impl LokView {
|
||||
}
|
||||
self.edited += u64::from(edits);
|
||||
let (outcome, now) = outcome;
|
||||
// A plain key took nothing; the menu may return.
|
||||
let taken = matches!(outcome, Ok(Some(Back::Completed(true))));
|
||||
self.completion.landed(seq, if taken { now } else { None });
|
||||
if edits {
|
||||
self.clock.landed(now);
|
||||
}
|
||||
@@ -118,7 +121,7 @@ impl LokView {
|
||||
self.part = part;
|
||||
self.invalidate(cx);
|
||||
}
|
||||
Ok(Some(Back::Shown(_))) => {}
|
||||
Ok(Some(Back::Shown(_) | Back::Completed(_))) => {}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
log::error!("lok: input failed ({:?})", error.kind());
|
||||
|
||||
@@ -4,6 +4,7 @@ mod borders;
|
||||
mod clip;
|
||||
mod commands;
|
||||
mod fit;
|
||||
mod formula;
|
||||
mod input;
|
||||
mod large;
|
||||
mod show_borders;
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
use super::*;
|
||||
use gpui::{Keystroke, TestAppContext};
|
||||
|
||||
fn settle(cx: &TestAppContext, done: impl Fn(&App) -> bool) -> bool {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while std::time::Instant::now() < deadline {
|
||||
if !cx.executor().tick() {
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_millis(20));
|
||||
}
|
||||
if cx.update(|app| done(app)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_completes_the_selected_function_without_leaving_the_cell() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
let (view, _) = cx.update(|app| {
|
||||
live_view_of(
|
||||
app,
|
||||
&install,
|
||||
"book.fods",
|
||||
crate::client::tests::input::FODS,
|
||||
)
|
||||
});
|
||||
let (_, window) = cx.add_window_view(|_, _| Host(view.clone()));
|
||||
window.run_until_parked();
|
||||
window.update(|window, app| {
|
||||
let focus = view.read(app).focus_handle().clone();
|
||||
focus.focus(window, app);
|
||||
});
|
||||
assert!(settle(window, |app| !view
|
||||
.read(app)
|
||||
.cell
|
||||
.address
|
||||
.is_empty()));
|
||||
let address = window.update(|_, app| view.read(app).cell.address.clone());
|
||||
view.update(window, |view, _| view.commit(None, "=lo"));
|
||||
assert!(settle(window, |app| {
|
||||
let functions = &view.read(app).cell.functions;
|
||||
functions.iter().any(|item| item.name == "LOOKUP")
|
||||
&& functions.iter().any(|item| item.name == "LOG")
|
||||
}));
|
||||
// Keep two valid offers to exercise keyboard selection.
|
||||
view.update(window, |view, cx| {
|
||||
view.cell
|
||||
.functions
|
||||
.retain(|item| item.name == "LOG" || item.name == "LOOKUP");
|
||||
cx.notify();
|
||||
});
|
||||
window.simulate_keystrokes("down enter");
|
||||
assert!(
|
||||
settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.formula == "=LOOKUP()" && view.cell.functions.is_empty()
|
||||
}),
|
||||
"Enter inserts the selected function"
|
||||
);
|
||||
window.update(|_, app| {
|
||||
assert_eq!(
|
||||
view.read(app).cell.address,
|
||||
address,
|
||||
"completion stays in the cell"
|
||||
);
|
||||
view.update(app, |view, _| {
|
||||
view.commit(None, "1,1,1");
|
||||
});
|
||||
});
|
||||
assert!(settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.formula == "=LOOKUP(1,1,1)"
|
||||
}));
|
||||
window.simulate_keystrokes("enter");
|
||||
assert!(
|
||||
settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.address != address
|
||||
}),
|
||||
"Enter without candidates commits and moves to the next cell"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepting_a_candidate_consumes_it_before_the_worker_answers() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let cx = TestAppContext::single();
|
||||
let (view, worker) = cx.update(|app| {
|
||||
live_view_of(
|
||||
app,
|
||||
&install,
|
||||
"book.fods",
|
||||
crate::client::tests::input::FODS,
|
||||
)
|
||||
});
|
||||
cx.update(|app| view.update(app, |view, _| view.commit(None, "=look")));
|
||||
assert!(settle(&cx, |app| view
|
||||
.read(app)
|
||||
.cell
|
||||
.functions
|
||||
.iter()
|
||||
.any(|item| item.name == "LOOKUP")));
|
||||
let held = worker.lock().expect("the worker lock");
|
||||
cx.update(|app| {
|
||||
view.update(app, |view, cx| {
|
||||
let old = Events {
|
||||
revision: view.doc.rev,
|
||||
cell: view.cell.clone(),
|
||||
..Events::default()
|
||||
};
|
||||
let before = view.sent();
|
||||
assert!(view.key_down(&Keystroke::parse("enter").expect("Enter")));
|
||||
view.apply_batch(old, None, cx);
|
||||
assert!(
|
||||
view.completion.pending(),
|
||||
"an old event must not reoffer an accepted candidate"
|
||||
);
|
||||
assert_eq!(view.sent(), before + 1);
|
||||
assert!(view.key_down(&Keystroke::parse("enter").expect("Enter")));
|
||||
assert_eq!(
|
||||
view.sent(),
|
||||
before + 2,
|
||||
"the second Enter is queued normally"
|
||||
);
|
||||
})
|
||||
});
|
||||
drop(held);
|
||||
assert!(
|
||||
settle(&cx, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.address == "A2"
|
||||
}),
|
||||
"the second Enter commits and moves down"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_or_tab_after_queued_typing_follows_the_kernel_not_the_menu() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
for (key, to) in [("enter", "A2"), ("tab", "B1")] {
|
||||
let cx = TestAppContext::single();
|
||||
let (view, worker) = cx.update(|app| {
|
||||
live_view_of(
|
||||
app,
|
||||
&install,
|
||||
"book.fods",
|
||||
crate::client::tests::input::FODS,
|
||||
)
|
||||
});
|
||||
cx.update(|app| view.update(app, |view, _| view.commit(None, "=sum")));
|
||||
assert!(settle(&cx, |app| view
|
||||
.read(app)
|
||||
.cell
|
||||
.functions
|
||||
.iter()
|
||||
.any(|item| item.name == "SUM")));
|
||||
// The worker waits; the menu still shows SUM.
|
||||
let held = worker.lock().expect("the worker lock");
|
||||
cx.update(|app| {
|
||||
view.update(app, |view, _| {
|
||||
for text in ["(", "1", ")"] {
|
||||
view.commit(None, text);
|
||||
}
|
||||
match key {
|
||||
"enter" => assert!(view.key_down(&Keystroke::parse(key).expect("a key"))),
|
||||
_ => view.next_field(),
|
||||
}
|
||||
})
|
||||
});
|
||||
drop(held);
|
||||
assert!(
|
||||
settle(&cx, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.address == to
|
||||
}),
|
||||
"{key} commits =sum(1) and moves to {to}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Host(Entity<LokView>);
|
||||
|
||||
impl gpui::Render for Host {
|
||||
fn render(
|
||||
&mut self,
|
||||
_: &mut gpui::Window,
|
||||
_: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_a_function_does_not_send_mouse_input_to_the_sheet() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
let (view, _) = cx.update(|app| {
|
||||
live_view_of(
|
||||
app,
|
||||
&install,
|
||||
"book.fods",
|
||||
crate::client::tests::input::FODS,
|
||||
)
|
||||
});
|
||||
let (_, window) = cx.add_window_view(|_, _| Host(view.clone()));
|
||||
window.run_until_parked();
|
||||
view.update(window, |view, _| view.commit(None, "=look"));
|
||||
assert!(settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.functions.len() == 1 && view.cell.functions[0].name == "LOOKUP"
|
||||
}));
|
||||
window.run_until_parked();
|
||||
let (on, address, sent) = window.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
let cell = view.screen_rect(view.cell.cursor.expect("the cell cursor"));
|
||||
(
|
||||
cell.bottom_left() + point(px(12.0), px(12.0)),
|
||||
view.cell.address.clone(),
|
||||
view.sent(),
|
||||
)
|
||||
});
|
||||
window.simulate_mouse_down(on, gpui::MouseButton::Left, Default::default());
|
||||
window.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
assert_eq!(view.sent(), sent, "the sheet never receives the press");
|
||||
assert!(
|
||||
!view.dragging,
|
||||
"the candidate press never starts a sheet drag"
|
||||
);
|
||||
});
|
||||
// Change candidates without repainting the already pressed row.
|
||||
view.update(window, |view, _| {
|
||||
view.cell.functions.insert(
|
||||
0,
|
||||
crate::Function {
|
||||
name: "LOG".into(),
|
||||
signature: String::new(),
|
||||
description: String::new(),
|
||||
},
|
||||
);
|
||||
});
|
||||
window.simulate_mouse_up(on, gpui::MouseButton::Left, Default::default());
|
||||
assert!(
|
||||
settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
view.settled() && view.cell.formula == "=LOOKUP()"
|
||||
}),
|
||||
"the candidate receives the click"
|
||||
);
|
||||
window.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
assert_eq!(view.cell.address, address, "the active cell stays put");
|
||||
assert_eq!(view.sent(), sent + 1, "only completion reaches the sheet");
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
use crate::ffi::{Document, Lok};
|
||||
use crate::ipc::{self, Pack, Unpack, tag};
|
||||
|
||||
mod complete;
|
||||
mod events;
|
||||
#[cfg(unix)]
|
||||
mod fatal;
|
||||
@@ -264,13 +265,7 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) {
|
||||
.and_then(|id| with_doc(id, |doc| doc.selection_text()))
|
||||
.and_then(|text| text)
|
||||
.map(|text| Pack::new().str(&text).bytes()),
|
||||
tag::COMPLETE_FUNCTION => unpack
|
||||
.u64()
|
||||
.and_then(|id| {
|
||||
let name = unpack.str()?;
|
||||
with_doc(id, |doc| doc.complete_function(&name)).and_then(|done| done)
|
||||
})
|
||||
.map(|()| Vec::new()),
|
||||
tag::COMPLETE_FUNCTION => complete::complete(unpack),
|
||||
tag::SET_AUTHOR => unpack
|
||||
.str()
|
||||
.and_then(|name| lok.set_option("author", &name))
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//! A function the kernel offers, taken in input order (TABLE-03).
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::with_doc;
|
||||
use crate::ipc::{Pack, Unpack};
|
||||
|
||||
/// Takes an offered function, else posts Enter or Tab; 1 if taken.
|
||||
pub(super) fn complete(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
let id = unpack.u64()?;
|
||||
let name = match unpack.u64()? {
|
||||
0 => None,
|
||||
_ => Some(unpack.str()?),
|
||||
};
|
||||
let (char_code, key_code) = (unpack.i64()?, unpack.i64()?);
|
||||
// No key: a click; else Enter (1280) or Tab (1282).
|
||||
if !matches!(key_code, 0 | 1280 | 1282) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"a completion takes Enter or Tab",
|
||||
));
|
||||
}
|
||||
// Earlier input's callbacks already landed here.
|
||||
let offered = crate::events::offered(id);
|
||||
let take = match name {
|
||||
Some(name) if offered.contains(&name) => Some(name),
|
||||
// As the menu orders them: shortest first.
|
||||
_ if key_code != 0 => offered.into_iter().min_by_key(String::len),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(name) = take {
|
||||
log::info!("lok-worker: completes a function");
|
||||
with_doc(id, |doc| doc.complete_function(&name))??;
|
||||
return Ok(Pack::new().u64(1).bytes());
|
||||
}
|
||||
if key_code != 0 {
|
||||
with_doc(id, |doc| {
|
||||
doc.post_key(0, char_code as i32, key_code as i32);
|
||||
doc.post_key(1, char_code as i32, key_code as i32);
|
||||
})?;
|
||||
}
|
||||
Ok(Pack::new().u64(0).bytes())
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
- 窗口变窄时,两侧栏先变窄,再先收起右栏、后收起左栏,文档区至少保留 360 点;窗口变宽即回来。侧栏的宽度可拖动内侧边缘调整,按应用记住。
|
||||
- 侧栏放不下全部文字时,分页按钮只显示图标,悬停可见名称。
|
||||
|
||||
## ElyTable 的公式补全
|
||||
|
||||
- 单元格内输入公式时,上下键选择函数候选,Tab、Enter 或单击候选接受函数并继续编辑参数。没有函数候选时,Enter 提交输入并移到下一格。
|
||||
- 数据有效性的下拉按钮保留当前单元格,选择的值写入该格;点击输入说明不切换到其后被遮住的单元格。
|
||||
|
||||
## ElyMark 的菜单与快捷键
|
||||
|
||||
- 段落与格式菜单照 Typora。菜单里带 Shift 的标点键写成所出的字符,如 Ctrl+{ 即 Ctrl+Shift+[。标题:Ctrl+1 至 Ctrl+6,Ctrl+0 段落,Ctrl+= 与 Ctrl+- 升降一级。块:Ctrl+Shift+K 代码块,Ctrl+Shift+M 公式块,Ctrl+Shift+Q 引用,Ctrl+Shift+[ 有序列表,Ctrl+Shift+] 无序列表,Ctrl+Shift+X 任务列表。
|
||||
|
||||
+4
-3
@@ -319,6 +319,7 @@
|
||||
- [x] R9.2 合并 PR #1(用户定):选区画在已画好的图块之上,Omarchy 等主题的选区色不透明时盖住所选文字;画选区时不透明度至多 0.3,更低的照用(`view/render.rs`)。合并前在 Windows 上跑其测试 `selection_keeps_document_text_readable_across_themes` 通过,去掉上限即以对比度 1:1 失败;在 GitHub 以合并提交并入(bddf765),本地 main 随之合并
|
||||
- [x] R9.3 #4 ElyTable 开自动筛选即内核崩溃(§9,用户 2026-10-20 定:发 0.1.9 前修好):选中整列(点列标)而该列看不出标题(全是文字或全空)时,Calc 以异步询问框问“首行作标题吗”;worker 在宣告它的回调里当场关掉,询问框的回调在半建成的对话框上加上筛选,下一次画图块即访问违例(Windows 0xc0000005,macOS SIGSEGV)。单格、数据块、首行、有数字列的整列都不问,不崩。补丁 `0053-lok-calc-autofilter-header-without-asking.patch`:LOK 下不问,首行作标题(询问框的默认,同 Excel)。测试:`client/tests/filter.rs` 的 `a_whole_column_without_a_clear_header_filters_from_its_first_row`(整列文字开筛选、画图块、条目不含首行)在未打补丁的内核上开筛选即失败。 审查两轮(与 R9.4 同审):第 1 轮一处注释超过十字,已改;第 2 轮通过。同时查出补丁 0049 用 `GetDoc()` 却未含 `doc.hxx`,Linux 的 gcc 编不过(MSVC 与 clang 经别的头文件带入),补上;三端源码树都为 53 个补丁,macOS 与 Linux 内核随之重建。
|
||||
- [x] R9.4 Windows 上粘贴不可用(用户 2026-10-20 报,提示只写“the clipboard reads”):arboard 3.6.1 在 Windows 读 HTML 时不先查有无该格式,剪贴板只有纯文字(记事本、浏览器地址栏等)时报未知错误而不是“无此内容”,`clipboard::read` 先读 HTML,任何纯文字粘贴都失败;macOS 与 Linux 照报“无此内容”。`ely_platform::clipboard` 在 Windows 先以 `IsClipboardFormatAvailable` 查 “HTML Format”;粘贴失败的提示写出整条原因(`{:#}`,原先只有最外一层)。测试:`clipboard.rs` 的往返测试写纯文字后读回、无 HTML;`view/tests/input.rs` 把外来纯文字粘进文档(都要 `ELY_CLIPBOARD_TEST=1`,会覆盖剪贴板);去掉查验即两者都失败。
|
||||
- [ ] R9.5 发布 0.1.9(用户 2026-10-20 定:先修好粘贴与 #2、#4 再发):首批六个应用;版本号,打标签由发布流程三端建包、签名公证、上传,发布说明中英两段
|
||||
- [ ] R9.6 Gitea 上的过渡发布(用户 2026-10-20 定):0.1.8 及更早的应用在 Gitea 查更新,在 Gitea 推上 main 与 v0.1.9,发同名发布、同样的文件;其清单指向 GitHub 的下载,0.1.9 起从 GitHub 查
|
||||
- [ ] R9.7 更新日志:0.1.9 发好后在用户桌面写中英两段的更新说明
|
||||
- [x] R9.5 GitHub PR #6(FMVPJet,用户 2026-10-20 定:发 0.1.9 前确认并合并):ElyTable 函数候选按 Enter 即提交半个公式并下移;候选、参数提示与有效性的下拉按钮、输入说明的按下落到其下的格。作者未编译未运行。本机:`view::tests::formula` 三项与 `tests::aids` 一项通过;去掉 Enter 接受、去掉候选的鼠标阻挡、去掉 ElyTable 浮层的阻挡,各自的测试都失败。PR 的 ElyTable 测试两处改正:规则缺错误样式(补丁 0017 拒绝,规则未设上),空单格读不出选区文字。第 1 轮:下拉按钮高 24、挡鼠标的框只有行高(默认行 17),按钮下缘仍落到格上;框至少为按钮的主题高度,测试按按钮右下角(去掉即失败)。第 2 轮:Enter 按上次回报的候选接受,其后已排队的键入未计(`=sum` 后快速键入 `(1)` 再 Enter 得 `=sum(1)SUM()`);改由 worker 在执行时按内核此刻的候选判定(`worker/complete.rs`),Tab 同;测试压住 worker 排队键入再按 Enter 与 Tab(去掉 worker 的判定即失败)。
|
||||
- [ ] R9.6 发布 0.1.9(用户 2026-10-20 定:先修好粘贴与 #2、#4 再发):首批六个应用;版本号,打标签由发布流程三端建包、签名公证、上传,发布说明中英两段
|
||||
- [ ] R9.7 Gitea 上的过渡发布(用户 2026-10-20 定):0.1.8 及更早的应用在 Gitea 查更新,在 Gitea 推上 main 与 v0.1.9,发同名发布、同样的文件;其清单指向 GitHub 的下载,0.1.9 起从 GitHub 查
|
||||
- [ ] R9.8 更新日志:0.1.9 发好后在用户桌面写中英两段的更新说明
|
||||
@@ -0,0 +1,3 @@
|
||||
FAIL
|
||||
|
||||
apps/ely-table/src/rules/aids.rs:115 — The mouse blocker covers the row-height wrapper (`size.max(16.0)` at line 125), while its `ControlSize::Sm` button is 24 px at standard density. On the default-height rows in the supplied E2 validation test, the button extends below that blocker, so pressing its lower edge still sends mouse input to the sheet underneath. Changing only the test's arrow coordinate to the button origin plus `(23 px, 23 px)` fails “the arrow press stays outside the grid”: `view.sent()` increases from 4 to 5. Expanding only the copied wrapper to 24 px makes that same test pass; logs are in `.work/r95-r1/probe-bottom-edge.log` and `probe-bottom-edge-expanded.log`. Make the blocking region cover the entire button using its actual themed size, and cover this lower-edge case in the regression test; the current test at `apps/ely-table/src/tests/aids.rs:67` samples only the row's center.
|
||||
@@ -0,0 +1,3 @@
|
||||
FAIL
|
||||
|
||||
crates/ely-lok/src/view/input.rs:273 — Enter accepts candidates from the last reported snapshot even when subsequent text is already queued; `completion.pending()` tracks accepted completions, not those edits. In A1, wait for the SUM candidate after typing `=sum`, then type `(1)` and press Enter before the worker reports the suffix. The isolated Windows probe produces `=sum(1)SUM()` and stays in A1 instead of committing and moving to A2. The same probe passes after removing only this new Enter branch (logs: `.work/r95-r2/probe-stale-enter-current.log`, `.work/r95-r2/probe-stale-enter-baseline.log`; probe source: `.work/r95-r2/probe-source.log`). Resolve acceptance against the state after preceding queued edits, and send ordinary Enter when no current candidate remains. Add a regression that delays the worker and queues a formula suffix followed by Enter.
|
||||
Reference in new issue
Block a user