Skip to content

Containers + bugfixes#87

Open
pimotte wants to merge 43 commits intomappingfrom
dev/variety-containers
Open

Containers + bugfixes#87
pimotte wants to merge 43 commits intomappingfrom
dev/variety-containers

Conversation

@pimotte
Copy link
Copy Markdown
Contributor

@pimotte pimotte commented Apr 2, 2026

This PR supersedes both #80 and #82.

27eacbc...dev/variety-containers allows viewing the changes that are new with respect to those PRs.

Besides container support, this contains a slew of bugfixes related to the mapping, in particular, nodeUpdates and line number display.

  • these bugs are both fixed
  • these imports were fixed
  • I removed the border of containers. I think it would be nice to show them in teacher mode, but in student mode is likely overkill. The pattern would be that the multilean correspond with exercises, we shouldn't span multilean over the entire document for elaboration reasons.

Testing

Edit both .mv and .lean documents, mainly focusing on inserting all nodes, wrapping/unwrapping input,hint and multilean.
Test in conjunction with the companion PR here: impermeable/waterproof-vscode#340

@pimotte pimotte marked this pull request as ready for review April 2, 2026 17:19
@pimotte pimotte requested a review from DikieDick April 2, 2026 17:19
This was referenced Apr 2, 2026
Copy link
Copy Markdown
Contributor

@DikieDick DikieDick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the example in the following document in a multilean block does not seem to update the line number of the code cell inside.

import WaterproofGenre

#doc (WaterproofGenre) "Index" =>

::::multilean
\`\`\`lean
example : True := sorry
\`\`\`
::::

I think something like this for replaceAroundReplace would work (note that I return the update linedelta, but to my understanding this is not used anywhere:

replaceAroundReplace(step: ReplaceAroundStep, tree: Tree): ParsedStep {
        // We start by checking what kind of node we are wrapping with
        const wrappingNode = step.slice.content.firstChild;
        if (!wrappingNode) {
            throw new NodeUpdateError(" ReplaceAroundStep replace has no wrapping node ");
        }

        const pmSize = step.slice.size;
        if (pmSize != 2) throw new NodeUpdateError(" Size of the slice is not equal to 2 ");
        
        if (step.slice.content.childCount != 1) {
            throw new NodeUpdateError(" We only support ReplaceAroundSteps with a single wrapping node ");
        }

        // Check that the wrapping node is of a supported type (hint, input, or container)
        const insertedNodeType = wrappingNode.type.name;
        if (insertedNodeType !== "hint" && insertedNodeType !== "input" && insertedNodeType !== "container") {
            throw new NodeUpdateError(" We only support wrapping in hints, inputs, or containers ");
        }

        // If we are wrapping in a hint node we need to have a title attribute; container uses name attribute
        const title: string = insertedNodeType === "hint" ? wrappingNode.attrs.title
            : insertedNodeType === "container" ? wrappingNode.attrs.name
            : "";
        // Get the tags for the wrapping node
        const [openTag, closeTag] = this.nodeNameToTagPair(insertedNodeType, title);

        // The step includes a range of nodes that are wrapped. We use the mapping
        // to find the node at gapFrom (the first one being wrapped) and the node
        // at gapTo (the last one being wrapped).
        let nodesBeingWrappedStart = tree.findNodeByProsePos(step.gapFrom);
        const nodesBeingWrappedEnd = tree.findNodeByProsePos(step.gapTo);
        // If one of the two doesn't exist we error
        if (!nodesBeingWrappedStart || !nodesBeingWrappedEnd) throw new NodeUpdateError(" Could not find node in mapping ");
        
        ////// NEW ////////
        const openTagLines = countNewlines(openTag);
        const closeTagLines = countNewlines(closeTag);
        /////// END NEW ///////

        // findNodeByProsePos is biased: at a boundary position it returns the node ENDING there.
        // If gapFrom equals nodesBeingWrappedStart.pmRange.to, we got the preceding node instead
        // of the node that starts at gapFrom. Advance to the next sibling to correct this.
        if (nodesBeingWrappedStart.pmRange.to === step.gapFrom) {
            const parent = tree.findParent(nodesBeingWrappedStart);
            const siblings = parent ? parent.children : tree.root.children;
            const idx = siblings.indexOf(nodesBeingWrappedStart);
            if (idx + 1 < siblings.length) {
                nodesBeingWrappedStart = siblings[idx + 1];
            }
        }

        // Generate the document change (this is a wrapping document change)
        const docChange: WrappingDocChange = {
            firstEdit: {
                finalText: openTag,
                startInFile: nodesBeingWrappedStart.tagRange.from,
                endInFile: nodesBeingWrappedStart.tagRange.from,
            }, 
            secondEdit: {
                finalText: closeTag,
                startInFile: nodesBeingWrappedEnd.tagRange.to,
                endInFile: nodesBeingWrappedEnd.tagRange.to
            }
        };

        // We now update the tree

        const positions = {
            startFrom: nodesBeingWrappedStart.tagRange.from, 
            startTo: nodesBeingWrappedStart.tagRange.to,
            endFrom: nodesBeingWrappedEnd.tagRange.from,
            endTo: nodesBeingWrappedEnd.tagRange.to,
            proseStart: nodesBeingWrappedStart.pmRange.from,
            proseEnd: nodesBeingWrappedEnd.pmRange.to
        };
        
        const contentEnd = positions.endTo + openTag.length;
        const tagEnd = positions.endTo + openTag.length + closeTag.length;

        // Create the new wrapping node
        const newNode = new TreeNode(
            insertedNodeType,
            {from: positions.startFrom + openTag.length, to: contentEnd}, // inner range
            {from: positions.startFrom, to: tagEnd}, // full range
            title,
            positions.proseStart + 1, positions.proseEnd + 1, // prosemirror start, end
            {from: positions.proseStart, to: positions.proseEnd + 2}, // pmRange
            0
        );

        // We need to find the parent of the first node being wrapped
        const parent = tree.findParent(nodesBeingWrappedStart);
        if (!parent) throw new NodeUpdateError(" Could not find parent of nodes being wrapped ");

        const nodesInRange = tree.nodesInProseRange(positions.proseStart, positions.proseEnd);

        // Remove the nodes that are now children of the new wrapping node from their current parent
        nodesInRange.forEach(n => {
            parent.removeChild(n);
        });
        
        // Finally we need to update all nodes that come after the inserted wrapping node
        tree.traverseDepthFirst((thisNode: TreeNode) => {
            if (thisNode.pmRange.from >= positions.proseEnd) {
                thisNode.shiftOffsets(openTag.length + closeTag.length, 2);
                /////// NEW ////////
                thisNode.shiftLineStart(openTagLines + closeTagLines);
                /////// END NEW ///////
            }
        });

        // Now we need to insert the new wrapping node in the right place in the tree
        parent.addChild(newNode);
        
        nodesInRange.forEach(n => {
            newNode.addChild(n);
            n.shiftOffsets(openTag.length, 1);
            //////// NEW /////////
            n.shiftLineStart(openTagLines);
             ////// END NEW //////
        });

        tree.root.shiftCloseOffsets(openTag.length + closeTag.length, 2);

        return {result: docChange, newTree: tree, lineDelta: openTagLines + closeTagLines};
    }

.container {
display: block;
position: relative;
padding-left: 8px;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinion: This extra padding is a bit ugly as it offsets the content further to the right. Can we make the distinction between container content and non container content visible in some other way? We could display an outline, but in teacher mode only? Do we really need to visually see where the containers are?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified to show a light dashed line, only for teachers.

@pimotte
Copy link
Copy Markdown
Contributor Author

pimotte commented Apr 13, 2026

Wrapping the example in the following document in a multilean block does not seem to update the line number of the code cell inside.

...

Fixed this along with the lifting case, which was also bugged.

pos = sel.from;
} else if (sel instanceof TextSelection) {
// TODO: This -1 is here to make sure that we do not insert 3 random code cells.
// TODO: This -1 is here to make sure that we do not insert 3 random code cells.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this todo be resolved here? I usually treat a todo as either solve it here, or put it in with an issue number, otherwise it should be just a plain comment I think.

} else if (sel instanceof TextSelection) {
// TODO: This -1 is here to make sure that we do not insert 3 random code cells.
// TODO: This -1 is here to make sure that we do not insert 3 random code cells.
// I can't fully wrap my head around why it is needed at the moment though.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, technically not part of this PR, but this comment feels a bit weird as well.

const sel = state.selection;
if (!(sel instanceof NodeSelection)) return false;
// Nesting containers is not allowed for now, because we want to disallow it for multilean
// TODO: Make this configurable
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be solved in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded comment to make it clearer. I think not, because I'm not sure if we will have containers that are nestable, and multilean is not.

new MarkdownBlock("B", {from: 14, to: 15}, {from: 14, to: 15}, 0)
];

configureNodeMock("A\n```coq\nX\n```B")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in this file, we introducing a lot of new references to the old coq/rocq name. I think we should avoid using the old term for code we change/update.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in this file I want to keep them for now, since these testcases (should) match what happens in actual files, and as of now, we still have the coq in the file format.

@pimotte
Copy link
Copy Markdown
Contributor Author

pimotte commented Apr 13, 2026

@DikieDick I either fixed or addressed your and Tammo's comments, so I think this is ready for a final check:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants