Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments (#281)

- review.previousComment / review.nextComment select the previous/next
  threaded comment in reading order (row by row), across visible sheets in
  tab order, wrapping around; disabled when the workbook has no comments.
- review.showComments {on?} toggles the Comments pane as session state, so
  the CLI, MCP and control channel can drive it; the UI pane follows it.
- review.showAllNotes {on?} displays every note (session display setting,
  not saved in the file).
- review.convertToComments turns every note in the workbook into a threaded
  comment with the same author and text, as one undoable edit.
- Ribbon: Previous/Next and a Show Comments toggle in the Comments group;
  Show All Notes and Convert to Comments in the Notes menu. Strings in all
  catalogs. Parity checklist regenerated (263 / 290).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76 2026-10-11 12:04:03 +02:00
parent 9f896c1249
commit 6f47a8b463
15 changed files with 270 additions and 25 deletions

View File

@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365)
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
@ -21,7 +21,7 @@ This page is the summary. The detail is in:
| Number | Value | Kind |
|---|---|---|
| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) |
| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 263 / 290 (90.7%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) |
| **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) |
| **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) |
| **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) |
@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | What landed |
|---|---|
| 2026-10-11 | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments (#281) |
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Catalog 258 → 263 / 290: Review comment navigation, Show Comments, Show All Notes, Convert to Comments (#281) |
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) |
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) |
| 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha |

View File

@ -1,6 +1,8 @@
//! Review tab: protection, spelling (basic), workbook statistics.
//! Review tab: protection, spelling (basic), workbook statistics, comment navigation and the
//! Comments pane, Show All Notes and Convert to Comments.
use gridcraft_model::{SheetProtection, password_hash};
use gridcraft_core::CellRef;
use gridcraft_model::{SheetProtection, Visibility, password_hash};
use serde_json::{Value as Json, json};
use super::*;
@ -20,9 +22,94 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("review.protectWorkbook", "Protect Workbook", ["Review", "Protect"], None, "{on?: bool}", has_doc, protect_workbook),
cmd!(query "review.workbookStatistics", "Workbook Statistics", ["Review", "Proofing"], None, "{}", has_doc, stats),
cmd!(query "review.checkAccessibility", "Check Accessibility", ["Review", "Accessibility"], None, "{} → issues", has_doc, accessibility),
cmd!(noundo "review.previousComment", "Previous Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| {
step_comment(s, false)
}),
cmd!(noundo "review.nextComment", "Next Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| step_comment(
s, true
)),
cmd!(query "review.showComments", "Show Comments", ["Review", "Comments"], None, "{on?: bool} (toggles the Comments pane)", has_doc, |s, p| {
s.comments_pane = bool_param(p, "on").unwrap_or(!s.comments_pane);
Ok(json!({"on": s.comments_pane}))
}),
cmd!(query "review.showAllNotes", "Show All Notes", ["Review", "Notes"], None, "{on?: bool}", has_doc, |s, p| {
s.show_all_notes = bool_param(p, "on").unwrap_or(!s.show_all_notes);
Ok(json!({"on": s.show_all_notes}))
}),
cmd!(
"review.convertToComments",
"Convert to Comments",
["Review", "Notes"],
None,
"{} (every note in the workbook) → {converted}",
has_doc,
convert_notes
),
]
}
/// Threaded comments in navigation order: visible sheets in tab order, then row by row.
fn comment_cells(s: &Session) -> Vec<(usize, CellRef)> {
let Some(d) = s.active() else { return vec![] };
d.wb.sheets
.iter()
.enumerate()
.filter(|(_, sh)| sh.visibility == Visibility::Visible)
// `CellRef` orders by row, then column: reading order.
.flat_map(|(i, sh)| sh.comments.iter().filter(|(_, c)| c.threaded).map(move |(at, _)| (i, *at)))
.collect()
}
fn has_comments(s: &Session) -> std::result::Result<(), String> {
has_doc(s)?;
if comment_cells(s).is_empty() { Err("there are no comments in this workbook".into()) } else { Ok(()) }
}
/// Selects the next (or previous) threaded comment after the active cell, moving on to the next
/// sheet and wrapping around at the end of the workbook.
fn step_comment(s: &mut Session, forward: bool) -> Result<Json> {
let list = comment_cells(s);
let d = s.doc()?;
let here = (d.wb.active_sheet, d.selection.active);
let target = if forward {
list.iter().find(|c| **c > here).or_else(|| list.first())
} else {
list.iter().rev().find(|c| **c < here).or_else(|| list.last())
};
let Some(&(sheet, at)) = target else {
return Err(EngineError::Other("There are no comments in this workbook.".into()));
};
let d = s.doc_mut()?;
d.wb_switch_sheet(sheet);
d.selection = crate::Selection::at(at);
if let Some(sh) = d.wb.active() {
d.selection.expand_merges(sh);
}
let name = d.wb.active().map(|sh| sh.name.clone()).unwrap_or_default();
Ok(json!({"sheet": name, "cell": at.a1()}))
}
/// Review › Notes › Convert to Comments: every note in the workbook becomes a threaded comment
/// with the same author and text.
fn convert_notes(s: &mut Session, _: &Json) -> Result<Json> {
let sheets: Vec<usize> =
s.doc()?.wb.sheets.iter().enumerate().filter(|(_, sh)| sh.comments.values().any(|c| !c.threaded)).map(|(i, _)| i).collect();
if sheets.is_empty() {
return Ok(json!({"converted": 0}));
}
edit(s, |cx| {
let mut n = 0usize;
for i in &sheets {
for c in cx.sheet_mut(*i)?.comments.values_mut().filter(|c| !c.threaded) {
c.threaded = true;
c.visible = false;
n += 1;
}
}
Ok(json!({"converted": n}))
})
}
fn protect_sheet(s: &mut Session, p: &Json) -> Result<Json> {
let sheet = target_sheet(s, p)?;
let b = |k: &str| bool_param(p, k).unwrap_or(false);
@ -97,3 +184,81 @@ fn accessibility(s: &mut Session, _: &Json) -> Result<Json> {
}
Ok(Json::Array(issues))
}
#[cfg(test)]
mod tests {
use super::*;
fn book() -> Session {
let mut s = Session::new();
s.new_workbook();
s.execute("home.insertSheet", json!({"name": "Two"})).unwrap();
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
for (sheet, cell) in [(0, "C1"), (0, "A3"), (1, "B2")] {
s.execute("review.newComment", json!({"sheet": sheet, "cell": cell, "text": "c", "author": "Ann"})).unwrap();
}
s.execute("review.newNote", json!({"sheet": 0, "cell": "A2", "text": "note", "author": "Bob"})).unwrap();
s
}
fn at(s: &Session) -> (usize, String) {
let d = s.doc().unwrap();
(d.wb.active_sheet, d.selection.active.a1())
}
#[test]
fn next_and_previous_comment_go_in_reading_order_across_sheets_and_wrap() {
let mut s = book();
s.execute("selection.set", json!({"cell": "B1"})).unwrap();
// Notes are skipped: A2 holds a note, not a comment.
let order = [(0, "C1"), (0, "A3"), (1, "B2"), (0, "C1")];
for (sheet, cell) in order {
s.execute("review.nextComment", json!({})).unwrap();
assert_eq!(at(&s), (sheet, cell.to_string()));
}
for (sheet, cell) in [(1, "B2"), (0, "A3"), (0, "C1")] {
s.execute("review.previousComment", json!({})).unwrap();
assert_eq!(at(&s), (sheet, cell.to_string()));
}
}
#[test]
fn comment_navigation_is_disabled_without_comments() {
let mut s = Session::new();
s.new_workbook();
s.execute("review.newNote", json!({"cell": "A1", "text": "note"})).unwrap();
assert!(s.execute("review.nextComment", json!({})).is_err());
assert!(s.execute("review.previousComment", json!({})).is_err());
assert_eq!(at(&s), (0, "A1".to_string()));
}
#[test]
fn convert_to_comments_turns_every_note_into_a_comment_and_undoes() {
let mut s = book();
s.execute("review.newNote", json!({"sheet": 1, "cell": "D4", "text": "second", "author": "Cy"})).unwrap();
s.execute("review.showNote", json!({"sheet": 1, "cell": "D4"})).unwrap();
let r = s.execute("review.convertToComments", json!({})).unwrap();
assert_eq!(r["converted"], 2);
let wb = &s.doc().unwrap().wb;
let a2 = &wb.sheets[0].comments[&CellRef::parse("A2").unwrap()];
let d4 = &wb.sheets[1].comments[&CellRef::parse("D4").unwrap()];
assert!(a2.threaded && d4.threaded && !d4.visible);
assert_eq!((a2.author.as_str(), a2.text.as_str(), d4.author.as_str(), d4.text.as_str()), ("Bob", "note", "Cy", "second"));
// Nothing left to convert; one undo restores the notes.
assert_eq!(s.execute("review.convertToComments", json!({})).unwrap()["converted"], 0);
s.execute("edit.undo", json!({})).unwrap();
assert!(!s.doc().unwrap().wb.sheets[0].comments[&CellRef::parse("A2").unwrap()].threaded);
}
#[test]
fn show_comments_and_show_all_notes_toggle() {
let mut s = book();
assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], true);
assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], false);
assert_eq!(s.execute("review.showComments", json!({"on": true})).unwrap()["on"], true);
assert!(s.comments_pane);
assert_eq!(s.execute("review.showAllNotes", json!({})).unwrap()["on"], true);
assert_eq!(s.execute("review.showAllNotes", json!({"on": false})).unwrap()["on"], false);
assert!(!s.show_all_notes);
}
}

View File

@ -200,6 +200,11 @@ pub struct Session {
pub draw_tool: String,
pub draw_color: String,
pub draw_width: f32,
/// Review › Show Comments: the Comments pane is open.
pub comments_pane: bool,
/// Review › Notes › Show All Notes: every note is displayed. An application display setting
/// (like Excel's comment indicator option), so it's not saved in the file.
pub show_all_notes: bool,
}
impl Session {

View File

@ -571,7 +571,8 @@ impl Dialog {
"saveChanges" => Dialog::custom("saveChanges", "Save Changes?", json!({"title": p("title")})),
"start" => Dialog::custom("start", "GridCraft", json!({})),
"comments" => {
app.grid.pane = Some("comments".into());
app.grid.pane = None;
app.run_or_alert("review.showComments", json!({"on": true}));
return None;
}
"watch" | "selectionPane" | "formatChart" => {

View File

@ -2001,8 +2001,9 @@ fn paint_overlays(
}
// Notes: shown on hover (or always when marked visible).
let hover = app.grid.hover_cell;
let all_notes = app.session.show_all_notes;
for (c, cm) in &sh.comments {
let show = cm.visible || hover == Some(*c);
let show = cm.visible || (all_notes && !cm.threaded) || hover == Some(*c);
if !show || app.grid.drag != Drag::None {
continue;
}

View File

@ -321,6 +321,12 @@ const JAPANESE: &[(&str, &str)] = &[
("Check\nAccessibility", "アクセシビリティ チェック"),
("Threaded Comments", "スレッド化されたコメント"),
("Show\nComments", "コメントの表示"),
("Previous", "前へ"),
("Next", "次へ"),
("Previous Comment", "前のコメント"),
("Next Comment", "次のコメント"),
("Show All Notes", "すべてのメモを表示"),
("Convert to Comments", "コメントに変換"),
("Protect\nWorkbook", "ブックの保護"),
("Unprotect\nSheet", "シート保護の解除"),
("Protect Sheet…", "シートの保護…"),

View File

@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Check\nAccessibility" => "접근성\n검사",
"Threaded Comments" => "스레드 댓글",
"Show\nComments" => "댓글\n표시",
"Previous" => "이전",
"Next" => "다음",
"Previous Comment" => "이전 댓글",
"Next Comment" => "다음 댓글",
"Show All Notes" => "모든 메모 표시",
"Convert to Comments" => "댓글로 변환",
"Protect\nWorkbook" => "통합 문서\n보호",
"Unprotect\nSheet" => "시트 보호\n해제",
"Protect Sheet…" => "시트 보호…",

View File

@ -173,6 +173,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Check\nAccessibility" => "Verificar\nAcessibilidade",
"Threaded Comments" => "Comentários Encadeados",
"Show\nComments" => "Mostrar\nComentários",
"Previous" => "Anterior",
"Next" => "Próximo",
"Previous Comment" => "Comentário Anterior",
"Next Comment" => "Próximo Comentário",
"Show All Notes" => "Mostrar Todas as Notas",
"Convert to Comments" => "Converter em Comentários",
"Protect\nWorkbook" => "Proteger\nPasta de Trabalho",
"Unprotect\nSheet" => "Desproteger\nPlanilha",
"Protect Sheet…" => "Proteger Planilha…",

View File

@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Check\nAccessibility" => "Проверка\nдоступности",
"Threaded Comments" => "Комментарии с ветками",
"Show\nComments" => "Показать\nкомментарии",
"Previous" => "Назад",
"Next" => "Далее",
"Previous Comment" => "Предыдущий комментарий",
"Next Comment" => "Следующий комментарий",
"Show All Notes" => "Показать все заметки",
"Convert to Comments" => "Преобразовать в комментарии",
"Protect\nWorkbook" => "Защитить\nкнигу",
"Unprotect\nSheet" => "Снять защиту\nлиста",
"Protect Sheet…" => "Защитить лист…",

View File

@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Check\nAccessibility" => "检查\n辅助功能",
"Threaded Comments" => "线程式批注",
"Show\nComments" => "显示\n批注",
"Previous" => "上一条",
"Next" => "下一条",
"Previous Comment" => "上一条批注",
"Next Comment" => "下一条批注",
"Show All Notes" => "显示所有备注",
"Convert to Comments" => "转换为批注",
"Protect\nWorkbook" => "保护\n工作簿",
"Unprotect\nSheet" => "撤销\n工作表保护",
"Protect Sheet…" => "保护工作表…",

View File

@ -7,7 +7,15 @@ use crate::SheetApp;
use crate::theme::{self, Tokens};
pub fn show(app: &mut SheetApp, ui: &mut Ui) {
let Some(pane) = app.grid.pane.clone() else { return };
// The Comments pane is engine state (`review.showComments`, so agents can drive it); the other
// panes are UI state. One pane shows at a time: opening another one closes Comments.
if app.grid.pane.as_deref() == Some("comments") {
app.grid.pane = None;
app.run_or_alert("review.showComments", json!({"on": true}));
} else if app.grid.pane.is_some() && app.session.comments_pane {
app.run_or_alert("review.showComments", json!({"on": false}));
}
let Some(pane) = app.grid.pane.clone().or_else(|| app.session.comments_pane.then(|| "comments".to_string())) else { return };
let t = Tokens::get(ui.ctx());
egui::Panel::right("task_pane").default_size(300.0).resizable(true).frame(egui::Frame::NONE.fill(t.ribbon).inner_margin(10)).show(ui, |ui| {
ui.horizontal(|ui| {
@ -21,6 +29,9 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) {
ui.label(egui::RichText::new(title).font(theme::ui_bold(15.0)));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if crate::widgets::icon_button(ui, crate::icons::Icon::Close, t.text_dim, "Close", vec2(22.0, 22.0)).clicked() {
if pane == "comments" {
app.run_or_alert("review.showComments", json!({"on": false}));
}
app.grid.pane = None;
}
});

View File

@ -1596,15 +1596,43 @@ fn review(app: &mut SheetApp, ui: &mut Ui) {
if big_button(ui, Icon::Comment, "New\nComment", "New Comment", false).clicked() {
app.open_dialog("comment", json!({"threaded": true}));
}
if big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false).clicked() {
app.grid.pane = Some("comments".into());
}
if big_button(ui, Icon::Delete, "Delete", "Delete Comment", false).clicked() {
act(app, "review.deleteComment", json!({}));
}
ui.vertical(|ui| {
for (icon, label, tip, id) in
[(Icon::Left, "Previous", "Previous Comment", "review.previousComment"), (Icon::Right, "Next", "Next Comment", "review.nextComment")]
{
if small_button(ui, icon, label, tip, false).clicked() {
act(app, id, json!({}));
app.grid.ensure_visible = true;
}
}
});
let shown = app.session.comments_pane;
let r = big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false);
if shown {
ui.painter().rect_stroke(r.rect, 5.0, Stroke::new(2.0, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
}
if r.clicked() {
act(app, "review.showComments", json!({"on": !shown}));
if app.session.comments_pane {
app.grid.pane = None;
}
}
let notes = big_button(ui, Icon::Note, "Notes", "Notes", true);
egui::Popup::menu(&notes).show(|ui| {
menu_items(app, ui, &[("New Note", "dialog:comment", json!({"threaded": false})), ("Show/Hide Note", "review.showNote", json!({}))]);
menu_items(
app,
ui,
&[
("New Note", "dialog:comment", json!({"threaded": false})),
("Show/Hide Note", "review.showNote", json!({})),
("Show All Notes", "review.showAllNotes", json!({})),
("-", "", json!({})),
("Convert to Comments", "review.convertToComments", json!({})),
],
);
});
sep(ui);
let protected = app.session.active().and_then(|d| d.wb.active().map(|s| s.is_protected())).unwrap_or(false);

View File

@ -1,6 +1,6 @@
# Gaps
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from the full re-measure against Excel for Mac 16.113.4 and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (comment navigation, Show All Notes and Convert to Comments landed, #281) · **Target:** Microsoft Excel (Microsoft 365)
Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone
using GridCraft for real work first). This is the work list: pick from the top, check the open
@ -110,8 +110,8 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
[function-parity.md](function-parity.md)
### 16. Collaboration and review (F)
- No co-authoring, Show Changes, Allow Edit Ranges, comment navigation, notes ↔ comments
conversion; cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner).
- No co-authoring, Show Changes, Allow Edit Ranges; threaded comments are saved to XLSX as notes
(no `threadedComments` part yet); cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner).
### 17. Insert objects (F)
- SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot; 8 shape kinds vs
@ -149,6 +149,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | §16: comment navigation, Show Comments, Show All Notes and Convert to Comments landed (#281) |
| 2026-10-10 | minor | Updated for the 10-10 merges (#17, #31, #38, #62, #82, #104, #116) and the new chart resize bug (#190) |
| 2026-10-10 | minor | Noted that no gap blocks the alpha gate |
| 2026-10-10 | major | Created: 24 ranked gaps with evidence, from the full re-measure and the 60 open issues |

View File

@ -4,7 +4,7 @@
Each entry is an Excel ribbon or menu command (feature names only). It counts as implemented when GridCraft registers a command with the same id. This measures breadth only; the overall assessment (depth, fidelity, effort) is in [target-app-parity.md](target-app-parity.md).
**Overall: 258 / 290 commands implemented (89.0%).**
**Overall: 263 / 290 commands implemented (90.7%).**
| Tab | Implemented | Total | Parity |
|---|---|---|---|
@ -15,7 +15,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as
| Page Layout | 23 | 23 | 100% |
| Formulas | 23 | 23 | 100% |
| Data | 20 | 28 | 71% |
| Review | 9 | 18 | 50% |
| Review | 14 | 18 | 78% |
| View | 18 | 20 | 90% |
| Automate | 3 | 3 | 100% |
| Table Design | 12 | 13 | 92% |
@ -276,7 +276,7 @@ Missing:
- `data.solver` Solver (Analysis)
- `data.analysisToolPak` Data Analysis (Analysis)
## Review (9 / 18, 50%)
## Review (14 / 18, 78%)
Implemented:
@ -285,8 +285,13 @@ Implemented:
- `review.workbookStatistics` Workbook Statistics (Proofing)
- `review.checkAccessibility` Check Accessibility (Accessibility)
- `review.deleteComment` Delete Comment (Comments)
- `review.previousComment` Previous Comment (Comments)
- `review.nextComment` Next Comment (Comments)
- `review.showComments` Show Comments (Comments)
- `review.newNote` New Note (Notes)
- `review.showNote` Show/Hide Note (Notes)
- `review.showAllNotes` Show All Notes (Notes)
- `review.convertToComments` Convert to Comments (Notes)
- `review.unprotectSheet` Unprotect Sheet (Protect)
- `review.protectWorkbook` Protect Workbook (Protect)
@ -294,11 +299,6 @@ Missing:
- `review.smartLookup` Smart Lookup (Insights)
- `review.translate` Translate (Language)
- `review.previousComment` Previous Comment (Comments)
- `review.nextComment` Next Comment (Comments)
- `review.showComments` Show Comments (Comments)
- `review.showAllNotes` Show All Notes (Notes)
- `review.convertToComments` Convert to Comments (Notes)
- `review.allowEditRanges` Allow Edit Ranges (Protect)
- `review.showChanges` Show Changes (Changes)

View File

@ -1,6 +1,6 @@
# Target-app parity: GridCraft vs Microsoft Excel
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365)
This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
[`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
@ -13,7 +13,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md).
| Number | Value | Kind |
|---|---|---|
| Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements |
| Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) |
| Ribbon and menu catalog | **263 / 290 (90.7%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) |
| Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) |
| Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) |
| **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) |
@ -90,7 +90,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2
| What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables. Missing: Solver, Analysis ToolPak, Forecast Sheet, Analyze Data |
| Get & Transform, external data, data model | 5 | ~15% | ~10% | 60–100 | Import of CSV/TSV, JSON, HTML. No Power Query editor, no From Web, database or folder sources, no connections or refresh, no relationships or data model, no external workbook links (dropped with a warning) |
| Insert objects | 3 | ~55% | ~45% | 25–40 | Pictures, 8 shape kinds (Excel has ~170), text boxes, own icon set, ink with Ink to Shape, hyperlinks, checkboxes. Missing: SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot, pictures in cells (PR #105) |
| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Missing: comment navigation, Show Changes, Allow Edit Ranges, notes-to-comments conversion, accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) |
| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Comment navigation, Show All Notes and Convert to Comments (#281). Missing: Show Changes, Allow Edit Ranges, threaded comments in XLSX (saved as notes), accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) |
| Automation | 4 | ~30% | ~20% | 12–20 | Recorded, editable GridCraft scripts (Automate tab). VBA is out of scope by owner decision and is **dropped** when an `.xlsm` is opened (warning shown). No Office Scripts compatibility |
| **Weighted** | 100 | **~75%** | **~59%** | **300–485** | |
@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments; catalog 258 → 263 / 290 (#281) |
| 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) |
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) |
| 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |