diff --git a/agents.md b/agents.md index 6023fc5..af45433 100644 --- a/agents.md +++ b/agents.md @@ -268,6 +268,6 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-09:ElyTable 的自动筛选(R2.9)。内核会重新推算未命名的自动筛选区:每次打开筛选列表时向下扩到紧邻的数据行(`ExtendDataArea`、`ExtendBackColorArea`,为接上后来追加的行),命令从单格找到它时按连续数据区重算(`ScDocShell::GetDBData`:应用条件、关闭筛选及其撤销);选区下方紧邻的合计行因此进了列表、随数据一起被隐藏,存后重开的筛选区同样会扩。补丁 `0040-lok-calc-autofilter-keeps-its-rows.patch`:LOK 下已有的自动筛选区保持设定时的行(同 Excel),不再隐式扩展(光标在区内时;光标在同表另一块数据里即按那一块);单格开筛选时仍取整个数据区。代价:紧贴在筛选区下方追加的行不自动并入,需重新选定后再开筛选。 - 2026-10-09:ElyTree 的差异按文件类型着色(R2.12)。组件库 7f2ac3e 至 b0dbfd4:`DiffViewer`(建立时按路径定下读法) 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同,不着色),代码按语言着色(组件库 b0dbfd4 的 `forms::syntax`:Rust、JavaScript/TypeScript、类 C、Python、Ruby、Shell、YAML、INI、SQL、Lua、Haskell、Lisp、Erlang、TeX 与其他 `#` 语言各有行注释记号、关键字与常量;Shell 与 YAML 的 `#` 只在词首起注释;Makefile、Dockerfile、CMakeLists.txt 先按文件名认),不是完整的语法分析器(字符串插值、块注释、多行字符串照通用词法)(原先一律套 `//` 的通用词法,Markdown 的 `#`、Python 的注释都被当作代码上色)。同批带入上游的块编辑器跨块选择、浮层滚轮、输入法候选框位置、插入点按设备像素对齐与格式修正。 - 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在一块的序列化与原文在把其中每张表格化成规范形式(各列对齐方式加各格的原文,`tables.js` 的 `canon`)后相同,即为可视块(`preserve.js` 的 `kept`),引用与列表里的表格同样;格内有 Milkdown 会改写的写法(`[[…]]`、引用式链接等)即不同,整块仍为原文块,与段落的规则一致。Milkdown 的列表项必须以段落开头,表格作列表项的第一块时无法放进编辑器,照原文块保留。未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 按表格的行节点投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票),分隔行随之重写(如 `|---|` 成 `| - |`)。 -- 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁、并挤回当前可见区;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表压在数据上或旁边的另一块数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:LOK 下有请求区域时,图表放进数据区右侧(隔一列)第一块够大、无合并格的空单元格(逐列向右找,跨过旁边的数据块),右侧没有依次找下方、左侧、上方(尺寸从候选格起累加,表尾也快),不受可见区限制;四周都没有空处时照内核原样;`LokView` 在新选中对象(原先无选中)时把它滚入视口,拖动中的对象不滚动。 +- 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁、并挤回当前可见区;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表压在数据上或旁边的另一块数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:LOK 下有请求区域时,图表放进数据区右侧(隔一列)第一块够大、无合并格的空单元格(逐列向右找,跨过旁边的数据块),右侧没有依次找下方、左侧、上方,再按行带找全表第一块够大的空处(尺寸从候选格起累加,表尾也快),不受可见区限制;空处按图表所在的工作表查,数据在别的表时从光标起找;整张表没有空处即插入失败、文档不动;`LokView` 在新选中对象(原先无选中)时把它滚入视口,拖动中的对象不滚动。 - 2026-10-09:ElyPDF 编辑与缩放不闪白(R2.6)。旧图块不再随编辑或比例变化即刻丢弃:编辑落地时全部当前图块、比例变化时离开视图的同页图块,连同当时的页面尺寸进入“底图”(`apps/ely-pdf/src/backdrop.rs`),按其在页面中的比例位置画在该页新图块之下,直到该页在视口里的新图块到齐、或页面尺寸与旋转变了才释放;每页画什么由 `PdfView::pieces` 决定,绘制与测试同用它。只有改内容的编辑保留底图并沿用已测的页面尺寸(前 100 页以外不再被占位尺寸覆盖);增删、移动、旋转、裁剪页面与撤销重做(`PdfView::edit_pages`)清空底图,因同一序号可能已是另一页。关闭标签时底图随图块交给窗口释放。原先比较整份尺寸表(超过 100 页的混合纸张文档尺寸表后段为占位,比较失败即全部清空)且只认同比例的图块(编辑后右栏收起、比例变化即清空),都会闪白。 - 2026-10-09:文档的缩放锚点(R2.7)。窄于视口的文档居中,缩放原按视口坐标求锚点、漏了居中的偏移,锚点随缩放漂移;现锚点先减去缩放前的偏移,缩放后按新的偏移(`LokView::slack`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。 diff --git a/crates/ely-lok/src/client/tests/analysis.rs b/crates/ely-lok/src/client/tests/analysis.rs index 271c14c..4310cac 100644 --- a/crates/ely-lok/src/client/tests/analysis.rs +++ b/crates/ely-lok/src/client/tests/analysis.rs @@ -336,7 +336,13 @@ fn undoing_a_new_kind_brings_the_whole_chart_back() { /// A sheet of ones filling each `(columns, rows)` block. fn blocks(filled: &[(std::ops::RangeInclusive, std::ops::RangeInclusive)]) -> String { - let rows: String = (1..=60) + sheet(&rows_of(filled)) +} + +/// Rows of ones filling each `(columns, rows)` block. +fn rows_of(filled: &[(std::ops::RangeInclusive, std::ops::RangeInclusive)]) -> String { + let height = filled.iter().map(|(_, rows)| *rows.end()).max().unwrap_or(0); + (1..=height) .map(|r| { let cells: String = (0..20) .map(|c| match filled.iter().any(|(cols, rows)| cols.contains(&c) && rows.contains(&r)) { @@ -346,12 +352,11 @@ fn blocks(filled: &[(std::ops::RangeInclusive, std::ops::RangeInclusive{cells}") }) - .collect(); - sheet(&rows) + .collect() } /// Charts `source` and checks it clears each data block. -fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&str, &str)]) { +fn charted(sheet: &str, visible: (i32, i32, i32, i32), (source, stand): (&str, &str), data: &[(&str, &str)]) { let (_dir, mut worker, doc) = open_book(sheet); doc.visible_area(&mut worker, visible, (256, 3840)).expect("the area"); let mut seen = poll_until(&mut worker, &doc, &Events::default(), SETTLE, |_| false); @@ -363,7 +368,7 @@ fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&s }; let spans: Vec<_> = data.iter().map(|(from, to)| (cell(&mut worker, from), cell(&mut worker, to))).collect(); let all = written(&mut worker, &doc, source); - worker.run(&doc, "cell.goto", &["A1"]).expect("A1"); + worker.run(&doc, "cell.goto", &[stand]).expect("where the chart goes"); worker.run(&doc, "chart.insert", &[&all, "false", &Look { kind: "column".into(), ..Look::default() }.to_json()]).expect("inserts"); let framed = poll_until(&mut worker, &doc, &seen, SETTLE, |now| now.graphic.is_some()); let (x, y, w, h) = framed.graphic.expect("the chart is selected"); @@ -375,12 +380,18 @@ fn charted(sheet: &str, visible: (i32, i32, i32, i32), source: &str, data: &[(&s /// A sheet body as flat XML. fn sheet(rows: &str) -> String { + sheets(&[("S", rows)]) +} + +/// Named sheets as one flat XML book. +fn sheets(tables: &[(&str, &str)]) -> String { + let tables: String = tables.iter().map(|(name, rows)| format!(r#"{rows}"#)).collect(); format!( r#" - {rows} + {tables} "# ) } @@ -393,19 +404,28 @@ fn a_new_chart_lands_beside_its_data() { return; } // Its own data, a neighbour past two empty columns. - charted(&blocks(&[(0..=2, 1..=5), (5..=15, 1..=20)]), (0, 0, 15000, 10000), "A1:C5", &[("A1", "C5"), ("F1", "P20")]); + charted(&blocks(&[(0..=2, 1..=5), (5..=15, 1..=20)]), (0, 0, 15000, 10000), ("A1:C5", "A1"), &[("A1", "C5"), ("F1", "P20")]); // Data wider and taller than the view. - charted(&blocks(&[(0..=15, 1..=50)]), (0, 0, 15000, 10000), "A1:P50", &[("A1", "P50")]); + charted(&blocks(&[(0..=15, 1..=50)]), (0, 0, 15000, 10000), ("A1:P50", "A1"), &[("A1", "P50")]); // A view narrower than the chart. - charted(&blocks(&[(0..=2, 1..=5)]), (0, 0, 6000, 3500), "A1:C5", &[("A1", "C5")]); + charted(&blocks(&[(0..=2, 1..=5)]), (0, 0, 6000, 3500), ("A1:C5", "A1"), &[("A1", "C5")]); // A merged block beside the data is taken, not free. let merged = format!( r#"{ONE}{ONE}{ONE}MERGED{ONE}{ONE}{ONE}"# ); - charted(&sheet(&merged), (0, 0, 15000, 10000), "A1:C5", &[("A1", "C5"), ("D1", "D1")]); + charted(&sheet(&merged), (0, 0, 15000, 10000), ("A1:C5", "A1"), &[("A1", "C5"), ("D1", "D1")]); // Data in the last cells: left or above. - let corner = format!( - r#"{ONE}{ONE}"# - ); - charted(&sheet(&corner), (0, 0, 15000, 10000), "XFC1048575:XFD1048576", &[("XFC1048575", "XFD1048576")]); + let corner = |above: usize| { + let empty = 1048574 - above; + format!( + r#"{ONE}{ONE}"# + ) + }; + charted(&sheet(&corner(0)), (0, 0, 15000, 10000), ("XFC1048575:XFD1048576", "A1"), &[("XFC1048575", "XFD1048576")]); + // Corner data, a block at A1: room elsewhere. + let full = format!("{}{}", rows_of(&[(0..=15, 1..=20)]), corner(20)); + charted(&sheet(&full), (0, 0, 15000, 10000), ("XFC1048575:XFD1048576", "A1"), &[("A1", "P20"), ("XFC1048575", "XFD1048576")]); + // Data on S, chart on T: T's cells stay clear. + let two = sheets(&[("S", &rows_of(&[(0..=2, 1..=5)])), ("T", &rows_of(&[(0..=15, 1..=20)]))]); + charted(&two, (0, 0, 15000, 10000), ("S!A1:C5", "T!A1"), &[("T!A1", "T!P20")]); } diff --git a/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch b/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch index 0e162aa..f2088ef 100644 --- a/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch +++ b/patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch @@ -5,12 +5,14 @@ request arguments (a kit client gives the range in FN_PARAM_5) was the cursor cell alone; GetChartInsertPos then pulled the chart back into the visible area. The chart covered the data it charts, or a block next to it. Under LOK, with a requested range, the chart goes in the -first empty, unmerged cells right of its data large enough for it, -else below, left or above, whatever the visible area. Sizes are summed -from the candidate cell, so a search near the sheet's end stays quick. +first empty, unmerged cells of its own sheet right of its data large +enough for it, else below, left or above, else the first such block +anywhere, whatever the visible area. Data on another sheet starts the +search at the cursor. With no room on the sheet the insert fails and +nothing lands. Sizes are summed from each candidate cell. diff --git a/sc/source/ui/drawfunc/fuins2.cxx b/sc/source/ui/drawfunc/fuins2.cxx -index b0169d0..2a6a2e9 100644 +index b0169d0..121500d 100644 --- a/sc/source/ui/drawfunc/fuins2.cxx +++ b/sc/source/ui/drawfunc/fuins2.cxx @@ -47,6 +47,8 @@ @@ -22,7 +24,7 @@ index b0169d0..2a6a2e9 100644 #include #include #include -@@ -215,6 +217,64 @@ void lcl_ChartInit(const uno::Reference & xObj, ScViewDa +@@ -215,6 +217,69 @@ void lcl_ChartInit(const uno::Reference & xObj, ScViewDa // don't create chart listener here (range may be modified in chart dialog) } @@ -81,13 +83,18 @@ index b0169d0..2a6a2e9 100644 + for (SCROW nRow = nTop - 2; oRight && nRow >= 0; --nRow) + if (const auto oEnd = lcl_Down(rDoc, nTab, nRow, nHeight); oEnd && *oEnd < nTop - 1 && free(nLeft, nRow, *oRight, *oEnd)) + return at(nLeft, nRow); ++ // Last, any chart-sized empty block, row bands first. ++ for (SCROW nRow = 0; const auto oBottom = lcl_Down(rDoc, nTab, nRow, nHeight); nRow = *oBottom + 1) ++ for (SCCOL nCol = 0; const auto oEnd = lcl_Across(rDoc, nTab, nCol, nWidth); nCol = *oEnd + 1) ++ if (free(nCol, nRow, *oEnd, *oBottom)) ++ return at(nCol, nRow); + return std::nullopt; +} + } FuInsertOLE::FuInsertOLE(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawView* pViewP, -@@ -452,6 +512,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV +@@ -452,6 +517,14 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV aRangeString = static_cast(pItem)->GetValue(); aPositionRange = rViewSh.GetViewData().GetCurPos(); @@ -102,7 +109,7 @@ index b0169d0..2a6a2e9 100644 } else { -@@ -612,7 +680,12 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV +@@ -612,7 +685,26 @@ FuInsertChart::FuInsertChart(ScTabViewShell& rViewSh, vcl::Window* pWin, ScDrawV // object position // get chart position (from window size and data range) @@ -110,8 +117,22 @@ index b0169d0..2a6a2e9 100644 + // LOK: beside the data, never over it, view or not. + std::optional oBeside; + if (pRange && comphelper::LibreOfficeKit::isActive()) -+ oBeside = lcl_EmptyBeside(rData.GetDocument(), aPositionRange, aSize); -+ // A sheet with no room around the data: as before. ++ { ++ // The chart's own sheet; data elsewhere starts at the cursor. ++ const SCTAB nTab = rData.CurrentTabForData(); ++ ScRange aNear = aPositionRange.aStart.Tab() == nTab ? aPositionRange : ScRange(rData.GetCurPos()); ++ aNear.aStart.SetTab(nTab); ++ aNear.aEnd.SetTab(nTab); ++ oBeside = lcl_EmptyBeside(rData.GetDocument(), aNear, aSize); ++ if (!oBeside) ++ { ++ SAL_WARN("sc.lok", "no empty cells hold the chart"); ++ if (xChartModel.is()) ++ xChartModel->unlockControllers(); ++ rViewSh.GetObjectShell()->GetEmbeddedObjectContainer().RemoveEmbeddedObject(aName); ++ return; ++ } ++ } + Point aStart = oBeside ? *oBeside : rViewSh.GetChartInsertPos( aSize, aPositionRange ); tools::Rectangle aRect (aStart, aSize); diff --git a/tasks/TASKS.md b/tasks/TASKS.md index 6570c56..e227162 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -187,7 +187,7 @@ - [x] R2.7 ElyDoc:页面水平位置每次一致;缩放以视口中的页面为锚,左页边不滚出视口 — R1 锚点漏居中偏移;R2 同帧连续缩放用旧偏移;R3 PASS - [ ] R2.8 ElyTable:状态栏统计按单元格格式显示数值(内核按光标格的数字格式写出,Ely 不改写;常规格式即内核的常规写法) — R3 FAIL(格式字面量里的分号被当作分隔)后改为按字段名切分,三轮上限,未再审 - [x] R2.9 ElyTable:对选中区域开自动筛选不扩到区域外紧邻的合计行 — R1 只标记于内存,应用、撤销、重开仍扩;R2 光标在别的数据块也沿用原区;R3 PASS -- [ ] R2.10 ElyTable:新插入的图表放在数据区旁的空白处,不压住数据 +- [ ] R2.10 ElyTable:新插入的图表放在数据区旁的空白处,不压住数据 — R1 只避开源区、被挤回可见区;R2 抛出异常使 worker 退出、合并格;R3 FAIL(跨表按源表查、四周无处时照旧压住数据)后改为按所在表查、全表找空处、无处即失败,三轮上限,未再审 - [x] R2.11 ElyTree:改动数的单复数(“1 file changed”) — R1 测试只核键名;R2 PASS - [ ] R2.12 ElyTree:差异按文件类型着色,Markdown 不按代码高亮 — R3 FAIL(Rust 与 Ruby 词表、Haskell 与 Erlang 注释、TOML 与 CMake 紧接注释)后按语言分表(组件库 b0dbfd4),三轮上限,未再审 - [ ] R2.13 ElyMark:紧凑写法的 GFM 表格(无对齐空格)在可视模式显示为表格 diff --git a/tasks/reviews/R2.10-r3.md b/tasks/reviews/R2.10-r3.md new file mode 100644 index 0000000..6ea59c9 --- /dev/null +++ b/tasks/reviews/R2.10-r3.md @@ -0,0 +1,5 @@ +FAIL + +patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch:53 — 上一轮的跨表定位缺陷仍存在:空白检测与坐标计算取数据源工作表,图表插入当前工作表。具体场景:S 表 A1:C5 填入数值,T 表 A1:P20 填满数值;在 T 表以 `S!$A$1:$C$5` 插入柱图。两次独立运行均生成矩形 (5120,0,9070,5102),覆盖 T 表已有数值的 F1 (6400,0,1279,255);保存后的 FODS 确认图表属于 T 表并保留该位置。修复方向:按实际插入工作表的几何与占用计算候选位置,补充跨表数据源且目标表已有数据的回归断言。证据:/tmp/ely-r210-r3-probes.log。 + +patches/libreoffice/0041-lok-calc-chart-beside-its-data.patch:115 — 搜索失败静默调用 GetChartInsertPos,重新引入视口内覆盖数据的行为。具体场景:同一表的 A1:P20 与 XFC1048575:XFD1048576 填入数值,可见区为 (0,0,15000,10000) twips,在 A1 以右下角四格插入柱图。搜索固定横向候选的起始行、纵向候选的起始列,右下角两个方向均容纳不了图表,大片空白也搜索不到;随后生成图表 (5872,4840,9071,5102),覆盖 F20 (6400,4864,1279,255)。两次独立运行及第三次带同 worker 另一文档的复验一致,FODS 保留该位置;另一文档仍可编辑,确认旧崩溃已消除。现有角落测试只核对源数据区,漏掉被覆盖的另一块数据。修复方向:在工作表边界同时调整候选行列以找到完整空白区域,搜索耗尽通过受控的命令失败上报,并让角落回归核对全部已有数据块。证据:/tmp/ely-r210-r3-probes.log、/tmp/ely-r210-r3-confirm.log。