Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
76cbcb3
Revert "Only show node action buttons in DEBUG mode"
pimotte Mar 11, 2026
cb19f82
Fix parent node selection bug
pimotte Mar 11, 2026
b2ff0ee
Add line number tests
pimotte Mar 11, 2026
e79b512
Fix line numbers
pimotte Mar 12, 2026
6e43d03
CodeGroup support for multilean block
pimotte Mar 16, 2026
f403457
Fix newline business
pimotte Mar 16, 2026
65715b9
Fix line numbers
pimotte Mar 16, 2026
7b0cdab
Remove unused method
pimotte Mar 16, 2026
6121555
Refactor to generic container
pimotte Mar 17, 2026
68c5850
Fixes
pimotte Mar 17, 2026
fd8900b
More tests
pimotte Mar 18, 2026
56046c9
Rename confusing names in schema
pimotte Mar 18, 2026
afb3b44
Fix newline bug
pimotte Mar 18, 2026
f755893
Fix newline and wrapping logic
pimotte Mar 18, 2026
10e43eb
Fix insertAbove bug and simplify sinert signatures
pimotte Mar 18, 2026
bb69451
Lint fix and remove multilean from generic container
pimotte Mar 18, 2026
2f2aeff
Refactor wrapping method
pimotte Mar 18, 2026
30364b7
Refactor tests
pimotte Mar 18, 2026
65f9cee
Remove codegroup mentions
pimotte Mar 18, 2026
571528b
Merge branch 'mapping' into feature/reenable-buttons
DikieDick Mar 24, 2026
68e2398
Merge remote-tracking branch 'origin/mapping' into feature/codegroup
pimotte Mar 26, 2026
5d81766
Add serializeFragment, update docs and count deleted newlines properly
DikieDick Mar 31, 2026
27eacbc
Merge branch 'feature/codegroup' into dev/merge-variety-containers
pimotte Apr 1, 2026
e43cd17
Fix newline management
pimotte Apr 1, 2026
63049dc
Fix merge
pimotte Apr 1, 2026
e60317e
Move to general function for container wrapping
pimotte Apr 1, 2026
e6fe7fa
Remove container decorator
pimotte Apr 1, 2026
c6d4bc6
Add regression test
pimotte Apr 1, 2026
c221a14
Fix deletion isText-detection bug
pimotte Apr 1, 2026
1879e8f
Fix stale prosedoc-multstep bug
pimotte Apr 2, 2026
f925b00
Fix deleting first code cell bug
pimotte Apr 2, 2026
dc34859
Document tricky search method
pimotte Apr 2, 2026
73144b9
Fix boundary check bug
pimotte Apr 2, 2026
c044b2e
Fix CodeBlockView management for lifting of containers
pimotte Apr 2, 2026
d7584b9
Refactor nodeUpdate API, proofreading
pimotte Apr 2, 2026
6f6c911
Remove multilean reference for container
pimotte Apr 2, 2026
afb3c84
Simplifying some stuff
pimotte Apr 2, 2026
675fdb1
Cleanup
pimotte Apr 2, 2026
6ba2aca
Fix test config
pimotte Apr 13, 2026
ae4c55e
Fix linecounting bugs
pimotte Apr 13, 2026
2c96075
Cleanup API
pimotte Apr 13, 2026
25397fd
Adopted teacher only display of multilean indicator
pimotte Apr 13, 2026
20b247a
Address review comments
pimotte Apr 13, 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
53 changes: 53 additions & 0 deletions __tests__/code-plugin-linenumbers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { EditorState } from "prosemirror-state";
import { CODE_PLUGIN_KEY, codePlugin } from "../src/codeview";
import { ThemeStyle } from "../src/api";
import { WaterproofSchema } from "../src/schema";

// The code plugin stores activeNodeViews in a Set<CodeBlockView>.
// After a ReplaceAroundStep (lift), ProseMirror may destroy and recreate a code
// NodeView. This test checks this is properly handled.

// Mock the INPUT_AREA_PLUGIN_KEY required by CodeBlockView
jest.mock('../src/inputArea.ts', () => ({
INPUT_AREA_PLUGIN_KEY: {
getState: jest.fn(() => ({ teacher: true }))
}
}));

test("After lift, stale NodeViews in activeNodeViews prevent line numbers from reaching cells", () => {
// Create a document with 3 code blocks (simulating post-lift state)
const doc = WaterproofSchema.nodes.doc.create({}, [
WaterproofSchema.nodes.code.create(null, WaterproofSchema.text("abc")),
WaterproofSchema.nodes.newline.create(),
WaterproofSchema.nodes.code.create(null, WaterproofSchema.text("def")),
WaterproofSchema.nodes.newline.create(),
WaterproofSchema.nodes.code.create(null, WaterproofSchema.text("ghi")),
]);

// Create the code plugin (dummy completions/symbols/editor — apply doesn't use them)
const plugin = codePlugin([], [], null as any, ThemeStyle.Light);
const state = EditorState.create({ schema: WaterproofSchema, plugins: [plugin], doc });

const pluginState = CODE_PLUGIN_KEY.getState(state);
if (!pluginState) throw new Error("Code plugin state not found");

// Simulate 3 real NodeViews + 1 stale view left over from lift
const mockViews = [
{ _getPos: () => 0, updateLineNumbers: jest.fn() }, // code1
{ _getPos: () => 6, updateLineNumbers: jest.fn() }, // code2 (recreated after lift)
{ _getPos: () => undefined, updateLineNumbers: jest.fn() }, // code2-old (stale — destroyed by PM, never removed)
{ _getPos: () => 12, updateLineNumbers: jest.fn() }, // code3
];
mockViews.forEach(v => pluginState.activeNodeViews.add(v as any));

// Dispatch line numbers computed by mapping.computeLineNumbers()
// (3 code blocks → 3 line numbers)
const lineNumbers = [1, 5, 9];
const tr = state.tr.setMeta(CODE_PLUGIN_KEY, lineNumbers);
state.apply(tr);

// EXPECTED: the 3 real code cells should each receive their line number.
expect(mockViews[0].updateLineNumbers).toHaveBeenCalledWith(2, false); // line 1 + 1
expect(mockViews[1].updateLineNumbers).toHaveBeenCalledWith(6, false); // line 5 + 1
expect(mockViews[3].updateLineNumbers).toHaveBeenCalledWith(10, false); // line 9 + 1
});
8 changes: 7 additions & 1 deletion __tests__/commands/all-insertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { configuration } from "../../src/markdown-defaults";
import { EditorView } from "prosemirror-view";
import { WaterproofSchema } from "../../src/schema";

/* Note that this is not a real tag configuration, the multilean is exclusive to lean files */
const tagConf: TagConfiguration = {
markdown: {
openTag: "", closeTag: "",
Expand All @@ -32,6 +33,11 @@ const tagConf: TagConfiguration = {
math: {
openTag: "$$", closeTag: "$$",
openRequiresNewline: false, closeRequiresNewline: false
},
container: {
openTag: (name: string) => `::::${name}\n`,
closeTag: "\n::::",
openRequiresNewline: false, closeRequiresNewline: false,
}
}

Expand All @@ -42,7 +48,7 @@ const initialStateMath = {"doc":{"type":"doc","content":[{"type":"math_display",
const startingCell: Array<[string, any]> = [
["Latex", initialStateMath],
["Markdown", initialStateMD],
["Code", initialStateCode]
["Code", initialStateCode],
];

const insertableTypes: Array<[string, (place: InsertionPlace, conf: TagConfiguration) => Command, string]> = [
Expand Down
119 changes: 118 additions & 1 deletion __tests__/commands/insert-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { EditorState, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { WaterproofSchema } from "../../src/schema";
import { getCmdInsertCode } from "../../src/commands/insert-command";
import { getCmdInsertCode, getCmdInsertMarkdown, getCmdInsertLatex } from "../../src/commands/insert-command";
import { InsertionPlace } from "../../src/commands";
import { configuration } from "../../src/markdown-defaults";

Expand All @@ -19,6 +19,52 @@ jest.mock('../../src/inputArea.ts', () => ({
}
}));

// A doc with a single code cell; selection inside the code content (position 11 = after 9 chars).
const stateOneCode = {"doc":{"type":"doc","content":[{"type":"code","content":[{"type":"text","text":"Goal True."}]}]},"selection":{"type":"text","anchor":11,"head":11}};

test("Insert markdown below code cell adds a newline separator", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateOneCode)});

const cmd = getCmdInsertMarkdown(InsertionPlace.Below, tagConf);
const res = cmd(view.state, view.dispatch, view);

expect(res).toBe(true);

// The newline node between code and markdown is required so that the serializer
// does not place markdown text on the same line as the closing code fence ("\n```").
const expected = {"doc":{"type":"doc",
"content":[
{"type":"code","content":[{"type":"text","text":"Goal True."}]},
{"type":"newline"},
{"type":"markdown"}
]},
"selection":{"type":"text","anchor":11,"head":11}};
expect(view.state.toJSON()).toStrictEqual(expected);
});

// A doc with a single code cell; selection inside the code content.
const stateOneCodeForAbove = {"doc":{"type":"doc","content":[{"type":"code","content":[{"type":"text","text":"Goal True."}]}]},"selection":{"type":"text","anchor":11,"head":11}};

test("Insert markdown above code cell adds a newline separator", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateOneCodeForAbove)});

const cmd = getCmdInsertMarkdown(InsertionPlace.Above, tagConf);
const res = cmd(view.state, view.dispatch, view);

expect(res).toBe(true);

// The newline node between markdown and code is required so that the serializer
// does not place markdown text on the same line as the opening code fence ("```lean\n").
const expected = {"doc":{"type":"doc",
"content":[
{"type":"markdown"},
{"type":"newline"},
{"type":"code","content":[{"type":"text","text":"Goal True."}]}
]},
"selection":{"type":"text","anchor":14,"head":14}};
expect(view.state.toJSON()).toStrictEqual(expected);
});

test("Insert code below twice (selection static)", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, state)});

Expand Down Expand Up @@ -53,7 +99,78 @@ test("Insert code below twice (selection static)", () => {
expect(view.state.toJSON()).toStrictEqual(newState2);
});

// States for testing insertion adjacent to code blocks using the real configuration
// (code has openRequiresNewline: true, closeRequiresNewline: true)
//
// doc: [code("Content.")][newline][math_display("Content.")]
// Positions: code size=10 (pos 0-10), newline size=1 (pos 10), math_display starts at pos 11
const stateCodeNewlineMath_mathSelected = {"doc":{"type":"doc","content":[{"type":"code","content":[{"type":"text","text":"Content."}]},{"type":"newline"},{"type":"math_display","content":[{"type":"text","text":"Content."}]}]},"selection":{"type":"node","anchor":11}};

// doc: [math_display("Content.")][newline][code("Content.")]
// math_display size=10 (pos 0-10), newline size=1 (pos 10), code starts at pos 11
const stateMathNewlineCode_mathSelected = {"doc":{"type":"doc","content":[{"type":"math_display","content":[{"type":"text","text":"Content."}]},{"type":"newline"},{"type":"code","content":[{"type":"text","text":"Content."}]}]},"selection":{"type":"node","anchor":0}};

test("Insert markdown above math_display when code is before the newline adds extra newline", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateCodeNewlineMath_mathSelected)});
const cmd = getCmdInsertMarkdown(InsertionPlace.Above, tagConf);
expect(cmd(view.state, view.dispatch, view)).toBe(true);

const content = view.state.toJSON().doc.content;
expect(content).toStrictEqual([
{"type":"code","content":[{"type":"text","text":"Content."}]},
{"type":"newline"},
{"type":"markdown"},
{"type":"newline"},
{"type":"math_display","content":[{"type":"text","text":"Content."}]}
]);
});

test("Insert math above math_display when code is before the newline adds extra newline", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateCodeNewlineMath_mathSelected)});
const cmd = getCmdInsertLatex(InsertionPlace.Above, tagConf);
expect(cmd(view.state, view.dispatch, view)).toBe(true);

const content = view.state.toJSON().doc.content;
expect(content).toStrictEqual([
{"type":"code","content":[{"type":"text","text":"Content."}]},
{"type":"newline"},
{"type":"math_display"},
{"type":"newline"},
{"type":"math_display","content":[{"type":"text","text":"Content."}]}
]);
});

test("Insert markdown below math_display when code is after the newline adds extra newline", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateMathNewlineCode_mathSelected)});
const cmd = getCmdInsertMarkdown(InsertionPlace.Below, tagConf);
expect(cmd(view.state, view.dispatch, view)).toBe(true);

const content = view.state.toJSON().doc.content;
expect(content).toStrictEqual([
{"type":"math_display","content":[{"type":"text","text":"Content."}]},
{"type":"newline"},
{"type":"markdown"},
{"type":"newline"},
{"type":"code","content":[{"type":"text","text":"Content."}]}
]);
});

test("Insert math below math_display when code is after the newline adds extra newline", () => {
const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, stateMathNewlineCode_mathSelected)});
const cmd = getCmdInsertLatex(InsertionPlace.Below, tagConf);
expect(cmd(view.state, view.dispatch, view)).toBe(true);

const content = view.state.toJSON().doc.content;
expect(content).toStrictEqual([
{"type":"math_display","content":[{"type":"text","text":"Content."}]},
{"type":"newline"},
{"type":"math_display"},
{"type":"newline"},
{"type":"code","content":[{"type":"text","text":"Content."}]}
]);
});
test("Insert code below twice (selection moves down)", () => {

const view = new EditorView(null, {state: EditorState.fromJSON({schema: WaterproofSchema}, state)});

const cmd = getCmdInsertCode(InsertionPlace.Below, tagConf);
Expand Down
Loading