generated from ellisonleao/nvim-plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
more efficient one-pass algorithm for diff update
- Loading branch information
Showing
9 changed files
with
594 additions
and
1,756 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
Oops, something went wrong.