bootstrapworld/codemirror-blocks

View on GitHub
packages/codemirror-blocks/src/edits/fakeAstEdits.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import * as P from "pretty-fast-pretty-printer";
import { warn, poscmp } from "../utils";
import { Required, Optional, List, Value, nodeSpec, value } from "../nodeSpec";
import { ASTNode } from "../ast";
import type { AST, Pos } from "../ast";
import { playSound, BEEP } from "../utils";
 
// Say that you have the source code `[1, 3]`, representing a list, and you
// insert `2` at a drop target in between `1` and `3`. We ultimately represent
// all edits as text edits, so what text edit shoudl this be?
//
// It can't just be to insert `2`, because that would be a syntax error.
// Instead, the insertion needs to include a comma, like `, 2` or `2, `.
//
// How do we get this edit? We don't want to ask every AST node to know how to
// deal with edits like this: that would be a lot to ask.
//
// Instead, we rely entirely on the existing `.pretty()` method. To make this
// edit, we will:
//
// 1. Clone the list node (whose source code is `[1, 3]`).
// 2. Create a FakeInsertNode with the text `2`.
// 3. Add this FakeInsertNode to the cloned list node. This cloned list node now
// has three children.
// 4. Call `.pretty()` on the cloned list node, producing the text `[1, 2, 3]`.
// (The FakeInsertNode's `.pretty()` method will just return "2".)
// 5. Construct a text edit from this: `[1, 3] -> [1, 2, 3]`.
// 6. Perform the text edit.
// 7. Re-parse the document. If the parse is successful, we replace the old AST
// with the newly parsed AST. If unsuccessful, we don't perform the edit.
// (Note that the cloned list node and the FakeInsertNode never appeared in
// any real AST: they weren't in the old AST, and they're not in the newly
// parsed AST either.)
//
// This file knows how to perform fake AST edits (steps 2&3).
 
// Knows how to perform a fake insertion operation, and how to find the inserted
// child after the AST is re-parsed.
export class FakeAstInsertion {
parent: ASTNode;
pos: Pos;
spec: List;
index: number;
Function `constructor` has a Cognitive Complexity of 9 (exceeds 5 allowed). Consider refactoring.
constructor(parent: ASTNode, fieldName: string, pos: Pos) {
this.parent = parent;
this.pos = pos;
// Find the spec that matches the supplied field name.
let spec: List | null = null;
for (const s of parent.spec.childSpecs) {
if (s instanceof List && s.fieldName === fieldName) {
spec = s;
}
}
if (spec === null) {
throw new Error(
"fakeAstEdits: Failed to find list to insert child into."
);
}
this.spec = spec;
// If `pos` is null, that means the list is empty.
if (pos === null) {
this.index = 0;
return;
}
// `pos` lies inside the list. Find out where.
const list = spec.getField(parent);
// ideally, we'd use for(i in list) {...} here, but some badly-behaved
// IDEs monkeypatch the Array prototype, causing that to fail
for (let i = 0; i < list.length; i++) {
if (poscmp(pos, list[i].srcRange().from) <= 0) {
this.index = i;
return;
}
}
this.index = list.length;
return;
}
 
insertChild(clonedParent: ASTNode, text: string) {
const newChildNode = fakeInsertNode(this.pos, this.pos, text);
this.spec.getField(clonedParent).splice(this.index, 0, newChildNode);
}
 
// Find the inserted child. If more than one was inserted at once, find the
// _last_.
// Since nodeIds are not stable across edits, findChild may fail if the
// parent node has been changed. In that case, return false to trigger
// the fallback focusHint handler.
findChild(newAST: AST) {
try {
const newParent = newAST.getNodeById(this.parent.id);
if (!newParent) return null;
const indexFromEnd = this.spec.getField(this.parent).length - this.index;
const newIndex = this.spec.getField(newParent).length - indexFromEnd - 1;
return this.spec.getField(newParent)[newIndex];
} catch (e) {
return false;
}
}
}
 
// Knows how to perform a fake replacement or deletion operation, and how to
// find the replaced child.
export class FakeAstReplacement {
parent: ASTNode;
child: ASTNode;
spec: Required | Optional | List;
index = 0;
Function `constructor` has a Cognitive Complexity of 15 (exceeds 5 allowed). Consider refactoring.
constructor(parent: ASTNode, child: ASTNode) {
this.parent = parent;
this.child = child;
for (const spec of parent.spec.childSpecs) {
if (spec instanceof List) {
const field = spec.getField(parent);
for (const i of field.keys()) {
if (field[i].id === child.id) {
this.spec = spec;
this.index = i;
return;
}
}
} else if (spec instanceof Required || spec instanceof Optional) {
if (spec.getField(parent)?.id === child.id) {
this.spec = spec;
return;
}
}
}
throw new Error(`Failed to find child to be replaced/deleted.`);
}
 
replaceChild(clonedParent: ASTNode, text: string) {
const newChildNode = fakeInsertNode(this.child.from, this.child.to, text);
if (this.spec instanceof List) {
this.spec.getField(clonedParent)[this.index] = newChildNode;
} else {
this.spec.setField(clonedParent, newChildNode);
}
}
 
deleteChild(clonedParent: ASTNode) {
if (this.spec instanceof List) {
this.spec.getField(clonedParent).splice(this.index, 1); // Remove the i'th element.
} else if (this.spec instanceof Optional) {
this.spec.setField(clonedParent, null);
} else {
playSound(BEEP);
this.spec.setField(
clonedParent,
fakeBlankNode(this.child.from, this.child.to)
);
}
}
 
// Call only if you used `replaceChild`, not `deleteChild`.
findChild(newAST: AST) {
const newParent = newAST.getNodeById(this.parent.id);
if (!newParent) return null;
if (this.spec instanceof List) {
return this.spec.getField(this.parent)[this.index];
} else {
return this.spec.getField(this.parent);
}
}
}
 
const fakeNodeSpec = nodeSpec([value<string, "text">("text")]);
// A fake ASTNode that just prints itself with the given text.
function fakeInsertNode(
from: Pos,
to: Pos,
text: string,
options = {}
): ASTNode<typeof fakeNodeSpec> {
return new ASTNode({
from,
to,
type: "fakeInsertNode",
fields: { text },
options,
pretty: (node) => {
const lines = node.fields.text.split("\n");
return P.vertArray(lines.map(P.txt));
},
render() {
warn("fakeAstEdits", "FakeInsertNode didn't expect to be rendered!");
},
spec: fakeNodeSpec,
});
}
 
function fakeBlankNode(from: Pos, to: Pos, options = {}): ASTNode {
return new ASTNode({
from,
to,
type: "fakeBlankNode",
fields: {},
options,
pretty: () => P.txt("..."),
render() {
warn("fakeAstEdits", "FakeBlankNode didn't expect to be rendered!");
},
spec: nodeSpec([]),
});
}
 
/**
* Make a copy of a node, to perform fake edits on (so that the fake edits don't
* show up in the real AST). This copy will be deep over the ASTNodes, but
* shallow over the non-ASTNode values they contain.
*/
Function `cloneNode` has a Cognitive Complexity of 12 (exceeds 5 allowed). Consider refactoring.
export function cloneNode(oldNode: ASTNode): ASTNode {
const nodeLike = { type: oldNode.type, fields: {} as any };
for (const spec of oldNode.spec.childSpecs) {
if (spec instanceof Required) {
spec.setField(nodeLike, cloneNode(spec.getField(oldNode)));
} else if (spec instanceof Optional) {
const field = spec.getField(oldNode);
if (field) {
spec.setField(nodeLike, cloneNode(field));
} else {
spec.setField(nodeLike, null);
}
} else if (spec instanceof Value) {
spec.setField(nodeLike, spec.getField(oldNode));
} else if (spec instanceof List) {
spec.setField(nodeLike, spec.getField(oldNode).map(cloneNode));
}
}
const cloned = new ASTNode({
from: oldNode.from,
to: oldNode.to,
...nodeLike,
type: oldNode.type,
options: oldNode.options,
pretty: oldNode._pretty,
render(_props: { node: ASTNode }) {
warn("fakeAstEdits", "cloned ASTNode didn't expect to be rendered!");
},
spec: oldNode.spec,
});
cloned.id = oldNode.id;
cloned._dangerouslySetHash(oldNode.hash);
return cloned;
}