Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
f0ef963
Add block-level three-way merge with a merger registry
XrxcGH Nov 7, 2026
9bfe161
Merge concurrent page saves by block when a merger opts in
XrxcGH Nov 7, 2026
3fadf39
Add a conflict view registry for block types
XrxcGH Nov 7, 2026
a47a52c
Record block-level merge on sync in ADR 0089
XrxcGH Nov 7, 2026
fddc3d4
Merge blocks only against the newest shared revision
XrxcGH Nov 7, 2026
caef668
Pick the block-merge base in save order and keep its snapshot reachable
XrxcGH Nov 7, 2026
ea8a5ac
Add comments, suggesting mode, and accept or reject
XrxcGH Nov 7, 2026
8104f15
Add the comments pane and the Review tab commands
XrxcGH Nov 7, 2026
826630e
Show formatting changes in the compare view
XrxcGH Nov 7, 2026
3ced63e
Record the review implementation in ADR 0086
XrxcGH Nov 7, 2026
b79bba4
Fit the review code into the bundle budget
XrxcGH Nov 7, 2026
d11b20b
List the review commands in the feature registry
XrxcGH Nov 7, 2026
60c3e10
Regenerate the feature list for the review commands
XrxcGH Nov 7, 2026
f12fc0c
Close the review verification gaps
XrxcGH Nov 7, 2026
035ff0a
Add the diagnostics scheduler with versioned results
XrxcGH Nov 11, 2026
117274c
Add the Python checker with pyflakes and pycodestyle codes
XrxcGH Nov 11, 2026
96a382d
Add the JavaScript and TypeScript checker with ESLint rule names
XrxcGH Nov 11, 2026
d6b4c5f
Add scope checkers for C, C++, and Lua
XrxcGH Nov 11, 2026
c716b1e
Run syntax and checker layers in a diagnostics worker
XrxcGH Nov 11, 2026
b289802
Shorten checker comments and split long test blocks
XrxcGH Nov 11, 2026
91c0062
Gate checker latency on a 1,000-line file
XrxcGH Nov 11, 2026
ff25ed2
Accept ADR 0074 with the Beta 12 implementation notes
XrxcGH Nov 11, 2026
02cc7d6
Load page parsers through the editor's language packs
XrxcGH Nov 11, 2026
2b4cb7b
Merge remote-tracking branch 'origin/beta' into b12-code-L10
XrxcGH Nov 11, 2026
875e29a
Rank completions and predict ghost text without a model
XrxcGH Nov 11, 2026
7b9cfed
Attach completion to code editors
XrxcGH Nov 11, 2026
43a6a4a
Merge remote-tracking branch 'origin/beta' into b12-code-L12
XrxcGH Nov 11, 2026
a4e4a53
Match the adapter stubs to the new RuntimeAdapter run signature
XrxcGH Nov 11, 2026
adf1ee1
Time the latency gate in a plain Node process
XrxcGH Nov 11, 2026
110119e
Add the JavaScript and TypeScript runtime on QuickJS
XrxcGH Nov 11, 2026
e220690
Merge remote-tracking branch 'origin/beta' into b12-code-L06
XrxcGH Nov 11, 2026
c7d4ab5
Merge origin/beta into b12-code-L06
XrxcGH Nov 11, 2026
af07efb
Fix false marks in the code checkers found by the L10 verification
XrxcGH Nov 11, 2026
e1e5dd4
Bound completion work and fix member, ghost, and step gaps
XrxcGH Nov 11, 2026
d4fbc7a
Fix the JavaScript runtime's limits, errors, and test API gaps
XrxcGH Nov 11, 2026
d4566c9
Merge remote-tracking branch 'origin/beta' into b12-code-L12
XrxcGH Nov 11, 2026
99d0766
Fix the L10 verification gaps in the Lua, C/C++, Python, and JS checkers
XrxcGH Nov 11, 2026
c3b6c33
Fix signature, window, word-cache, and SQL member gaps
XrxcGH Nov 11, 2026
ddd4e1d
Fix the JavaScript runtime's memory, guard, and test API gaps
XrxcGH Nov 11, 2026
4fd0489
Add the shared Python run session, prelude, and traceback mapping
XrxcGH Nov 11, 2026
f9ddda6
Add the quick Python adapter on bundled MicroPython
XrxcGH Nov 11, 2026
b7ead58
Add the full Python adapter on the Pyodide pack
XrxcGH Nov 11, 2026
3a5c404
Register both Python runtimes and test them
XrxcGH Nov 11, 2026
7cda230
Send early output at once and report prefilled-only runtimes
XrxcGH Nov 12, 2026
9cdea25
Fix Python run ends, the memory limit, and module shadowing
XrxcGH Nov 12, 2026
6c2de1e
Cap Python file data, forget Pyodide's proxies, and harden run ends
XrxcGH Nov 12, 2026
45f35d1
Merge remote-tracking branch 'origin/beta' into b12-code-L05
XrxcGH Nov 12, 2026
bbfea3b
Raise the start-up budget by a kilobyte for the Python run strings
XrxcGH Nov 12, 2026
3fdf0ce
Merge b12-code-L05 into train-rcode
XrxcGH Nov 15, 2026
2581792
Merge b12-code-L06 into train-rcode
XrxcGH Nov 15, 2026
19d2462
Merge b12-code-L10 into train-rcode
XrxcGH Nov 15, 2026
639f77e
Merge b12-code-L12 into train-rcode
XrxcGH Nov 15, 2026
882d3d4
Merge b12-shared-x-blockmerge into train-rcode
XrxcGH Nov 15, 2026
76a90bc
Merge b12-text-review into train-rcode
XrxcGH Nov 15, 2026
a139cf4
Index ADR 0089 and note block merge in format spec 14.1
XrxcGH Nov 15, 2026
9c5aa59
Wire the Python engine check and the JS runtime globals into diagnostics
XrxcGH Nov 15, 2026
5699efc
Give the tests typecheck MicroPython's module declaration
XrxcGH Nov 15, 2026
48aa9d7
Move the review command sentences to their own file
XrxcGH Nov 15, 2026
30f8ad5
Leave Track changes unbound in the Google Docs shortcut set
XrxcGH Nov 15, 2026
1c274f0
Document the rcode lanes: README, FEATURES, help pages
XrxcGH Nov 15, 2026
ac568aa
Budget the review lane's start-up code and Sucrase's chunk
XrxcGH Nov 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ You keep notebooks, sections, and pages in a notes folder you choose, as plain f

### Write and draw

The Word-level text, Excel-level table, and code studio foundations are on the beta branch behind flags that are off. The text work so far adds a Format pane with every font and paragraph field, a ruler for indents and tab stops, and a language and a direction for each passage. The table work so far adds a cell format bar, spreadsheet keys, conditional formatting, styled tables in Word and HTML exports, and the logic, lookup, array, and statistics functions.
The Word-level text, Excel-level table, and code studio foundations are on the beta branch behind flags that are off. The text work so far adds a Format pane with every font and paragraph field, a ruler for indents and tab stops, a language and a direction for each passage, and comments and suggested changes. The table work so far adds a cell format bar, spreadsheet keys, conditional formatting, styled tables in Word and HTML exports, and the logic, lookup, array, and statistics functions. The code studio work so far adds Python, JavaScript, and TypeScript runs with tests, error detection, and completion without a model. Sync can merge a page by block once a block type has a merger.

- [Typed notes](docs/help/typed-notes.md): text, headings, text styles kept with the notebook, lists, checklists, tables, code, callouts, Markdown, templates, and find and replace. Find by format, special character, wildcard, or regular expression. The Home tab has font and size boxes, split buttons for underline, colors, and lists, and a styles gallery. Typed web and e-mail addresses turn into links. Pasted tables and spreadsheet cells become tables. Copies give other apps plain text.
- [Moving between blocks](docs/help/moving-between-blocks.md): keys that move between text, pictures, tables, and code without the mouse.
- [Document structure](docs/help/document-structure.md): equation numbers and references, formula sheets, practice mode, problem-set templates, and figures.
- [Code cells](docs/help/code-cells.md): short Python or JavaScript programs that run on the page; a cell from a newer version opens read-only instead of being changed.
- Code projects (Beta and Nightly builds): several files and tests in one page block, shown as a small card with Run and Open in Code studio, with .zip export and import; search finds their code.
- Code projects (Beta and Nightly builds): several files and tests in one page block, shown as a small card with Run and Open in Code studio, with .zip export and import; search finds their code. Help for [Python](docs/help/python-projects.md) and [JavaScript and TypeScript](docs/help/javascript-and-typescript-projects.md) projects.
- [Ink and palm rejection](docs/help/ink-and-palm-rejection.md): pens, a highlighter, erasers, the lasso, shapes, and gestures, with your palm resting on the screen.
- [My handwriting](docs/help/my-handwriting.md): typed text and math written in your own hand, learned from a calibration page.
- [Paper and page views](docs/help/paper-and-page-views.md): flow, paginated, and canvas pages, with lined, grid, and dot paper; slides, print, and PDF.
Expand Down
4 changes: 3 additions & 1 deletion app/bundle-budget.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"startup": {
"jsKb": 620,
"jsKb": 622,
"cssKb": 30
},
"after": [
Expand All @@ -16,6 +16,7 @@
"core/audio",
"editor/",
"features/docstructure/",
"features/review/",
"features/ink/",
"features/pdf/"
],
Expand Down Expand Up @@ -71,6 +72,7 @@
"theme/fonts/caladea": 8,
"node_modules/mathlive/mathlive.min.mjs": 226,
"node_modules/pdfjs-dist/build/pdf.mjs": 131,
"node_modules/sucrase": 46,
"core/audio/index.ts": 117,
"features/accounts/googleOffice/commands.ts": 96,
"features/accounts/lms/commands.ts": 297,
Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/src/shellqol/conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub fn call(app: &AppHandle, name: &str, args: &Value) -> IpcResult<Value> {
Ok(json!({
"mine": handle.text(),
"theirs": handle.conflict_text(revision).map_err(core_error)?,
"blocks": handle.conflict_blocks(revision).map_err(core_error)?,
}))
}
"conflict.resolve" => {
Expand Down
52 changes: 52 additions & 0 deletions app/src/editor/diff/formatDiff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Formatting-aware compare (text DESIGN.md 9; ADR 0086). The expected notes are worked by hand from the two versions:
// the text is the same, so each difference is a mark that covers some text in one version only, or a `paras` key
// (spec 6.8) with another value.
import { describe, expect, it } from 'vitest';
import { describeFormatting } from './formatDiff';
import { diffPages, diffText } from './pageDiff';
import type { DiffBlock } from './pageDiff';

const text = (markdown: string, extra: Record<string, unknown> = {}): DiffBlock => ({
id: 'b',
type: 'text',
order: 'a0',
data: { markdown, ...extra },
});

describe('formatting-aware compare', () => {
it("shows 'Formatting changed: bold added' for two versions that differ only by bold", () => {
const [change] = diffText('Plants use photosynthesis daily.', 'Plants use **photosynthesis** daily.');
expect(change.status).toBe('changed');
expect(change.words?.every((part) => part.kind === 'same')).toBe(true);
expect(change.formatting).toEqual([{ kind: 'mark', change: 'added', mark: 'bold', text: 'photosynthesis' }]);
expect(describeFormatting(change.formatting!)).toBe('Formatting changed: bold added to “photosynthesis”');
});

it('names marks removed and changed', () => {
const [change] = diffText(
'*Leaf* and <span data-font="Georgia">stem</span>',
'Leaf and <span data-font="Arial">stem</span>',
);
expect(describeFormatting(change.formatting!)).toBe(
'Formatting changed: italic removed from “Leaf”; font changed on “stem”',
);
});

it('compares paras per key: an alignment change alone makes a changed paragraph', () => {
const ids = ['01m3sbpara000000000000p001'];
const before = text('Centered title', { ids, paras: { [ids[0]]: { align: 'start', before: 6 } } });
const after = text('Centered title', { ids, paras: { [ids[0]]: { align: 'center', before: 6 } } });
const [block] = diffPages({ blocks: [before] }, { blocks: [after] }).blocks;
expect(block.status).toBe('changed');
const paragraph = block.kind === 'text' ? block.paragraphs[0] : null;
expect(paragraph?.formatting).toEqual([{ kind: 'para', key: 'align', before: 'start', after: 'center' }]);
expect(describeFormatting(paragraph!.formatting!)).toBe('Formatting changed: alignment: center');
});

it('keeps equal paragraphs with equal paras the same', () => {
const ids = ['01m3sbpara000000000000p001'];
const one = text('Same', { ids, paras: { [ids[0]]: { align: 'center' } } });
const [block] = diffPages({ blocks: [one] }, { blocks: [one] }).blocks;
expect(block.status).toBe('same');
});
});
194 changes: 194 additions & 0 deletions app/src/editor/diff/formatDiff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Formatting-aware compare (text DESIGN.md 9; ADR 0086). When a paragraph's text is the same in two versions but its
// Markdown or its paragraph formatting differs, the compare view says what changed: "bold added to 'photosynthesis'",
// "alignment: center". Marks compare character by character; `paras` entries compare key by key, so a later
// paragraph merge can combine them per key (RFC 7396).
import type { Mark, Node as PMNode } from '@tiptap/pm/model';
import { entryFromAttrs } from '../paragraph/attributes';
import { ELEMENT_TYPES } from '../schema/specs';
import { t } from '../../strings/t';
import type { MessageKey } from '../../strings/t';

/** A mark that covers some text in one version and not in the other, or with other values. */
export interface MarkNote {
kind: 'mark';
change: 'added' | 'removed' | 'changed';
mark: string;
text: string;
}

/** A paragraph formatting key with another value, or none, in the newer version. */
export interface ParaNote {
kind: 'para';
key: string;
before: unknown;
after: unknown;
}

export type FormatNote = MarkNote | ParaNote;

/** One character's marks, by mark name; a block boundary or a leaf node is a character too. */
type Cell = { char: string; marks: ReadonlyMap<string, Mark> };

function cells(node: PMNode): Cell[] {
const out: Cell[] = [];
let first = true;
node.descendants((child) => {
if (child.isTextblock && !first) out.push({ char: '\n', marks: new Map() });
if (child.isTextblock) first = false;
if (!child.isInline) return true;
const marks = new Map(child.marks.map((mark) => [mark.type.name, mark]));
const text = child.isText ? (child.text ?? '') : '';
for (const char of text) out.push({ char, marks });
return false;
});
return out;
}

function stateOf(before: Mark | undefined, after: Mark | undefined): MarkNote['change'] | null {
if (!before && !after) return null;
if (!before) return 'added';
if (!after) return 'removed';
return before.eq(after) ? null : 'changed';
}

/** The text's runs that each mark was added to, removed from, or changed on, in text order. */
export function markNotes(before: PMNode, after: PMNode): MarkNote[] {
const a = cells(before);
const b = cells(after);
if (a.length !== b.length || a.some((cell, i) => cell.char !== b[i].char)) return [];
const names = new Set<string>();
for (const cell of [...a, ...b]) for (const name of cell.marks.keys()) names.add(name);
const runs: (MarkNote & { at: number })[] = [];
for (const mark of names) {
let open: (MarkNote & { at: number }) | null = null;
for (let i = 0; i <= a.length; i++) {
const change = i < a.length ? stateOf(a[i].marks.get(mark), b[i].marks.get(mark)) : null;
if (open && change === open.change && a[i].char !== '\n') {
open.text += a[i].char;
continue;
}
if (open) runs.push(open);
open = change && a[i].char !== '\n' ? { kind: 'mark', change, mark, text: a[i].char, at: i } : null;
}
}
return runs
.sort((x, y) => x.at - y.at || x.mark.localeCompare(y.mark))
.map(({ kind, change, mark, text }) => ({ kind, change, mark, text: text.trim() || text }));
}

/** The `paras` entries of a top-level block's elements, in document order. */
export function paraEntries(node: PMNode): Record<string, unknown>[] {
const out: Record<string, unknown>[] = [];
const add = (one: PMNode) => {
if (!ELEMENT_TYPES.has(one.type.name)) return;
const entry = entryFromAttrs(one.attrs);
out.push(entry && typeof entry === 'object' && !Array.isArray(entry) ? (entry as Record<string, unknown>) : {});
};
add(node);
node.descendants((child) => {
add(child);
return !child.isTextblock;
});
return out;
}

const same = (x: unknown, y: unknown) => JSON.stringify(x) === JSON.stringify(y);

/** The `paras` keys whose values differ between two blocks' elements, key by key. */
export function paraNotes(before: PMNode, after: PMNode): ParaNote[] {
const a = paraEntries(before);
const b = paraEntries(after);
const notes: ParaNote[] = [];
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const was = a[i] ?? {};
const now = b[i] ?? {};
for (const key of [...new Set([...Object.keys(was), ...Object.keys(now)])].sort()) {
if (!same(was[key], now[key])) notes.push({ kind: 'para', key, before: was[key], after: now[key] });
}
}
return notes;
}

/** A signature of a block's paragraph formatting, for telling equal blocks apart. */
export function paraSignature(node: PMNode): string {
const entries = paraEntries(node);
return entries.some((entry) => Object.keys(entry).length > 0) ? JSON.stringify(entries) : '';
}

const MARK_LABELS = new Set([
'link',
'comment',
'insertion',
'deletion',
'formatChange',
'dir',
'lang',
'charStyle',
'bold',
'weight',
'italic',
'strike',
'underline',
'highlight',
'shade',
'textColor',
'font',
'textSize',
'caps',
'tracking',
'scale',
'shift',
'kern',
'otf',
'effect',
'box',
'subscript',
'superscript',
'code',
]);

const PARA_LABELS = new Set([
'align',
'indent',
'before',
'after',
'contextual',
'line',
'keepNext',
'keepLines',
'breakBefore',
'widows',
'tabs',
'border',
'shade',
'dir',
'lang',
'outline',
'dropCap',
'list',
'hyphenate',
]);

const markLabel = (mark: string) =>
t(`textReview.marks.${MARK_LABELS.has(mark) ? mark : 'other'}` as MessageKey & `textReview.marks.${string}`);
const paraLabel = (key: string) =>
t(`textReview.paras.${PARA_LABELS.has(key) ? key : 'other'}` as MessageKey & `textReview.paras.${string}`);

const valueText = (value: unknown) => (typeof value === 'string' ? value : JSON.stringify(value));

/** One note in words, such as "bold added to “photosynthesis”" or "alignment: center". */
export function describeNote(note: FormatNote): string {
if (note.kind === 'para') {
if (note.after === undefined) return t('textReview.paraRemoved', { key: paraLabel(note.key) });
return t('textReview.paraChanged', { key: paraLabel(note.key), value: valueText(note.after) });
}
const values = { mark: markLabel(note.mark), text: note.text };
if (note.change === 'added') return t('textReview.markAdded', values);
if (note.change === 'removed') return t('textReview.markRemoved', values);
return t('textReview.markChanged', values);
}

/** The compare view's line for a paragraph whose formatting changed: "Formatting changed: bold added to …". */
export function describeFormatting(notes: readonly FormatNote[]): string {
return t('textReview.formattingChanged', { details: notes.map(describeNote).join('; ') });
}
Binary file modified app/src/editor/diff/pageDiff.ts
Binary file not shown.
85 changes: 85 additions & 0 deletions app/src/features/codestudio/complete/bounds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Hostile and large inputs (DESIGN 10.2, lane code:L12). Completion runs on the main thread after each key. A pasted
// or opened file must never stall it. Long runs of spaces, very long lines, and very large files each finish within
// a small bound, and the list stays short.
import { pythonLanguage } from '@codemirror/lang-python';
import { describe, expect, it } from 'vitest';
import { contextAt } from './ghost';
import { predict } from './pattern';
import { MAX_ITEMS, rank } from './rank';

/** Milliseconds one call takes. */
function timed(run: () => unknown): number {
const start = performance.now();
run();
return performance.now() - start;
}

const listAfter = (language: string, text: string) => rank(contextAt(language, text, text.length));

describe('hostile lines before a member access', () => {
const spaces = ' '.repeat(5000);

it('stays fast in Python, JavaScript, and C++', () => {
const cases: Array<[string, string]> = [
['python', `self: ${spaces}\nself.`],
['python', `self: list${spaces}x\nself.`],
['python', `${`self: ${' '.repeat(200)}x\n`.repeat(200)}self.`],
['javascript', `let self${spaces}: x${spaces}\nself.`],
['javascript', `const self:${spaces}x${spaces}\nself.`],
['cpp', `vector${spaces}${'vector <a '.repeat(500)}\nself.`],
['cpp', `std::vector${spaces}&${spaces}x\nself.`],
];
for (const [language, text] of cases) expect(timed(() => listAfter(language, text))).toBeLessThan(100);
});

it('reads a type from a line of ordinary length after a hostile one', () => {
const text = `nums: ${' '.repeat(5000)}\nnums = [1, 2]\nnums.`;
expect(listAfter('python', text).map((item) => item.label)).toContain('append');
});
});

describe('large files', () => {
const lines: string[] = [];
for (let i = 0; i < 60_000; i++) lines.push(`value_${i} = compute_${i}(value_${i - 1}) + offset`);
const big = lines.join('\n');

it('caps the list and answers quickly in a 60,000-line file', () => {
const general = `${big}\nval`;
const items = listAfter('python', general);
expect(items.length).toBeLessThanOrEqual(MAX_ITEMS);
expect(items.length).toBeGreaterThan(0);
expect(timed(() => listAfter('python', `${big}\nva`))).toBeLessThan(150);
expect(timed(() => listAfter('python', `${big}\nunknown.`))).toBeLessThan(150);
});

it('caps the scope walk with a tree', () => {
const text = `${big}\ndef last(radius):\n return rad`;
const tree = pythonLanguage.parser.parse(text);
rank(contextAt('python', text, text.length), { tree });
const longer = `${text}i`;
const fresh = pythonLanguage.parser.parse(longer);
let top = '';
expect(
timed(() => (top = rank(contextAt('python', longer, longer.length), { tree: fresh })[0]!.label)),
).toBeLessThan(150);
expect(top).toBe('radius');
}, 60_000);

it('reads a bounded window next to a multi-megabyte line', () => {
const dump = `CREATE TABLE big (a INT);\nINSERT INTO big VALUES ${'(1),'.repeat(1_750_000)}(1);\n`;
const select = `${dump}SELECT * FROM `;
const other = `${dump}x = 1\nx`;
// The editor's document text is one flat string; a search flattens the joined test texts the same way.
select.indexOf('\u0001');
other.indexOf('\u0001');
expect(timed(() => predict('sql', select, select.length, false))).toBeLessThan(50);
expect(timed(() => predict('python', other, other.length, true))).toBeLessThan(15);
});

it('keeps the pattern rules fast on long lines', () => {
const arrows = `def f(x) ${') -> '.repeat(16_000)}`;
expect(timed(() => predict('python', arrows, arrows.length, true))).toBeLessThan(50);
const run = `${'a1 = 1\n'.repeat(3)}${'x'.repeat(100_000)}`;
expect(timed(() => predict('python', run, run.length, true))).toBeLessThan(50);
});
});
Loading
Loading