Skip to content

Commit

Permalink
more efficient one-pass algorithm for diff update
Browse files Browse the repository at this point in the history
  • Loading branch information
dlants committed Dec 13, 2024
1 parent 5dd4031 commit 1fc5c8c
Show file tree
Hide file tree
Showing 9 changed files with 594 additions and 1,756 deletions.
13 changes: 8 additions & 5 deletions rplugin/node/magenta/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";


/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
Expand All @@ -19,8 +18,12 @@ export default [
},
{
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'no-void': ['error', { allowAsStatement: true }]
}
}
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ varsIgnorePattern: "^_" },
],
"no-void": ["error", { allowAsStatement: true }],
},
},
];
85 changes: 85 additions & 0 deletions rplugin/node/magenta/src/tea/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Line } from "../part.js";
import { calculatePosition, replaceBetweenPositions } from "./util.js";
import { MountedVDOM, MountPoint, VDOMNode } from "./view.js";

export async function render({
vdom,
mount,
}: {
vdom: VDOMNode;
mount: MountPoint;
}): Promise<MountedVDOM> {
type NodePosition =
| {
type: "string";
content: string;
start: number;
end: number;
}
| {
type: "node";
template: TemplateStringsArray;
children: NodePosition[];
start: number;
end: number;
};

// First pass: build the complete string and create tree structure with positions
let content = "";

function traverse(node: VDOMNode): NodePosition {
if (node.type === "string") {
const start = content.length;
content += node.content;
return {
type: "string",
content: node.content,
start,
end: content.length,
};
} else {
const start = content.length;
const children = node.children.map(traverse);
return {
type: "node",
template: node.template,
children,
start,
end: content.length,
};
}
}

const positionTree = traverse(vdom);

await replaceBetweenPositions({
...mount,
lines: content.split("\n") as Line[],
});

const mountPos = mount.startPos;
function assignPositions(node: NodePosition): MountedVDOM {
const startPos = calculatePosition(mountPos, content, node.start);
const endPos = calculatePosition(mountPos, content, node.end);

if (node.type === "string") {
return {
type: "string",
content: node.content,
startPos,
endPos,
};
} else {
const children = node.children.map(assignPositions);
return {
type: "node",
template: node.template,
children,
startPos,
endPos,
};
}
}

return assignPositions(positionTree);
}
136 changes: 136 additions & 0 deletions rplugin/node/magenta/src/tea/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { render } from "./render.js";
import {
ComponentVDOMNode,
MountedVDOM,
MountPoint,
Position,
StringVDOMNode,
VDOMNode,
} from "./view.js";

export async function update({
currentRoot,
nextRoot,
mount,
}: {
currentRoot: MountedVDOM;
nextRoot: VDOMNode;
mount: MountPoint;
}): Promise<MountedVDOM> {
// keep track of the edits that have happened in the doc so far, so we can apply them to future nodes.
const accumulatedEdit: {
deltaRow: number;
deltaCol: number;
lastEditRow: number;
} = {
deltaRow: 0,
deltaCol: 0,
lastEditRow: 0,
};

function updatePos(curPos: Position) {
const pos = { ...curPos };
if (pos.row == accumulatedEdit.lastEditRow) {
pos.row += accumulatedEdit.deltaRow;
pos.col += accumulatedEdit.deltaCol;
} else {
pos.row += accumulatedEdit.deltaRow;
}
return pos;
}

function updateNodePos(node: MountedVDOM): MountedVDOM {
return {
...node,
startPos: updatePos(node.startPos),
endPos: updatePos(node.endPos),
};
}

async function replaceNode(
current: MountedVDOM,
next: VDOMNode,
): Promise<MountedVDOM> {
// shift the node based on previous edits, so we replace the right range.
const nextPos = updateNodePos(current);

// replace the range with the new vdom
const rendered = await render({
vdom: next,
mount: {
...mount,
startPos: nextPos.startPos,
endPos: nextPos.endPos,
},
});

const oldEndPos = current.endPos;
const newEndPos = rendered.endPos;

if (newEndPos.row > oldEndPos.row) {
accumulatedEdit.deltaRow += newEndPos.row - oldEndPos.row;
// things on this endRow at pos X are at delta = X - oldEndPos.col
// they will now be in a new row at newEndPos.col + delta
// = X + newEndPos.col - oldEndPos.col
// so we need to save newEndPos.col - oldEndPos.col
accumulatedEdit.deltaCol = newEndPos.col - oldEndPos.col;
} else {
// this is a single-line edit. We just need to adjust the column
accumulatedEdit.deltaCol += newEndPos.col - oldEndPos.col;
}

accumulatedEdit.lastEditRow = oldEndPos.row;
return rendered;
}

async function visitNode(
current: MountedVDOM,
next: VDOMNode,
): Promise<MountedVDOM> {
if (current.type != next.type) {
return await replaceNode(current, next);
}

switch (current.type) {
case "string":
if (current.content == (next as StringVDOMNode).content) {
return updateNodePos(current);
} else {
return await replaceNode(current, next);
}

case "node": {
const nextNode = next as ComponentVDOMNode;
// have to update startPos before processing the children since we assume that positions are always processed
// in document order!
const startPos = updatePos(current.startPos);
const nextChildren = [];
if (current.template == nextNode.template) {
if (current.children.length != nextNode.children.length) {
throw new Error(
`Expected VDOM components with the same template to have the same number of children.`,
);
}

for (let i = 0; i < current.children.length; i += 1) {
const currentChild = current.children[i];
const nextChild = nextNode.children[i];
nextChildren.push(await visitNode(currentChild, nextChild));
}

const nextMountedNode = {
...current,
children: nextChildren,
startPos,
endPos: nextChildren[nextChildren.length - 1].endPos,
};
return nextMountedNode;
} else {
return await replaceNode(current, next);
}
}
}
}

return await visitNode(currentRoot, nextRoot);
}
49 changes: 49 additions & 0 deletions rplugin/node/magenta/src/tea/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Neovim, Buffer } from "neovim";
import { Position } from "./view.js";
import { Line } from "../part.js";

export async function replaceBetweenPositions({
nvim,
buffer,
startPos,
endPos,
lines,
}: {
nvim: Neovim;
buffer: Buffer;
startPos: Position;
endPos: Position;
lines: Line[];
}) {
await buffer.setOption("modifiable", true);
await nvim.call("nvim_buf_set_text", [
buffer.id,
startPos.row,
startPos.col,
endPos.row,
endPos.col,
lines,
]);
await buffer.setOption("modifiable", true);
}

export function calculatePosition(
startPos: Position,
text: string,
indexInText: number,
): Position {
let { row, col } = startPos;
let currentIndex = 0;

while (currentIndex < indexInText) {
if (text[currentIndex] === "\n") {
row++;
col = 0;
} else {
col++;
}
currentIndex++;
}

return { row, col };
}
Loading

0 comments on commit 1fc5c8c

Please sign in to comment.