diff --git a/demos/tree-sitter/matches.md b/demos/tree-sitter/matches.md
new file mode 100644
index 000000000..ca53ac28d
--- /dev/null
+++ b/demos/tree-sitter/matches.md
@@ -0,0 +1,38 @@
+# Matches
+
+
\ No newline at end of file
diff --git a/src/client/Falleri2014FGA_alorighm1.png b/src/client/Falleri2014FGA_alorighm1.png
new file mode 100644
index 000000000..e1fd35abc
Binary files /dev/null and b/src/client/Falleri2014FGA_alorighm1.png differ
diff --git a/src/client/domain-code.js b/src/client/domain-code.js
index 1df18333c..38ffcd20f 100644
--- a/src/client/domain-code.js
+++ b/src/client/domain-code.js
@@ -12,18 +12,25 @@ MD*/
import tinycolor from 'src/external/tinycolor.js';
-import {Parser, JavaScript} from "src/client/tree-sitter.js"
+import {Parser, JavaScript, visit as treeSitterVisit} from "src/client/tree-sitter.js"
import {loc} from "utils"
-export function treesitterVisit(node, func) {
- func(node)
- for(let i=0; i< node.childCount; i++) {
- let ea = node.child(i)
- treesitterVisit(ea, func)
- }
-}
-
+// from: https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
+const cyrb53 = (str, seed = 0) => {
+ let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
+ for(let i = 0, ch; i < str.length; i++) {
+ ch = str.charCodeAt(i);
+ h1 = Math.imul(h1 ^ ch, 2654435761);
+ h2 = Math.imul(h2 ^ ch, 1597334677);
+ }
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
+};
export class DomainObject {
@@ -40,6 +47,10 @@ export class DomainObject {
return true
}
+ get isTreeSitter() {
+ return false
+ }
+
get isReplacement() {
return false
}
@@ -87,8 +98,14 @@ export class DomainObject {
**Strategy**: write tests and then go either way and see if we arrive there...
MD*/
- static updateFromTreeSitter(rootNode, treeSitterNode) {
- debugger
+ static updateFromTreeSitter(rootNode, treeSitterNode, edit) {
+
+ // #TODO since TreeSitter does not reuse ids or update the old tree lets do it ourself
+ // we can find the corresbonding new AST nodes (for simple edits) by using the poisition
+ // (and modify with delta in edit accordingly)
+
+
+
let usedDomainObjects = new Set()
let removedDomainObjects = new Set()
let addedDomainObjects = new Set()
@@ -106,7 +123,7 @@ export class DomainObject {
})
- var newRootNode = TreeSitterDomainObject.fromTreeSitterAST(treeSitterNode, domainObjectsById, usedDomainObjects)
+ var newRootNode = TreeSitterDomainObject.fromTreeSitterAST(treeSitterNode)
for(let replacement of replacementsForDomainObject.values()) {
@@ -129,6 +146,116 @@ export class DomainObject {
rootNode.treeSitter = newRootNode.treeSitter
rootNode.children = newRootNode.children
}
+
+
+ static adjustIndex(index, edit) {
+
+ const delta = edit.newEndIndex - edit.oldEndIndex
+ if (index > edit.startIndex) {
+ return index + delta
+ } else {
+ return index
+ }
+ }
+
+ static edit(rootDomainObject, sourceNew, edit ) {
+
+ let { startIndex, oldEndIndex, newEndIndex } = edit
+
+ // 1. detect editit history (diff oldTree -> newTree)
+ // a) deleted nodes from oldTree
+ // b) added nodes in new tree
+
+ // 2. apply diff to domain tree
+
+
+ function assert(b) { if (!b) throw new Error() }
+
+ // #TODO use incremental re-parse via edit()
+ const newTree = TreeSitterDomainObject.astFromSource(sourceNew)
+
+ var tsQueue = [newTree.rootNode]
+ var doQueue = [rootDomainObject]
+
+ while (tsQueue.length > 0) {
+ const tsNode = tsQueue.pop();
+ const doNode = doQueue.pop();
+ assert(tsNode.type === doNode.type);
+
+ const lostChildren = []
+ const missingOldChildren = [...doNode.children]
+
+ // go over all new children, if we find a new child without an old child, create a new one
+ // if we do find a match, update the treeSitter reference
+ for (let i = 0; i < tsNode.childCount; i++) {
+ const tsChild = tsNode.child(i)
+ const doChild = doNode.children.find(child => tsChild.text === child.treeSitter.text)
+ if (!doChild) {
+ lostChildren.push([i, tsChild])
+ } else {
+ doChild.treeSitter = tsChild
+ missingOldChildren.splice(missingOldChildren.indexOf(tsChild), 1)
+ }
+ }
+
+ for (const [i, tsChild] of lostChildren) {
+ // we didn't find the exact child but maybe we can keep some of its children, if the
+ // type of the node is still the same (e.g., it's still a function)
+ const candidate = missingOldChildren.find(old => old.treeSitter.type === tsChild.type)
+ if (candidate) {
+ tsQueue.push(tsChild)
+ doQueue.push(candidate)
+ missingOldChildren.splice(missingOldChildren.indexOf(candidate), 1)
+ } else {
+ doNode.children.splice(i, 0, TreeSitterDomainObject.fromTreeSitterAST(tsChild))
+ }
+ }
+
+ for (const missing of missingOldChildren) {
+ doNode.children.splice(doNode.children.indexOf(missing), 1)
+ }
+ }
+
+
+ /*
+ const treeSitterNodesByLocationAndHash = new Map();
+ const remainingNewTreeSitterNodes = new Set()
+ treeSitterVisit(newTree.rootNode, node => {
+ let hash = cyrb53(node.text)
+ let locAndHash = node.startIndex + "-" + node.endIndex + "-" + hash
+ treeSitterNodesByLocationAndHash.set(locAndHash, node)
+ remainingNewTreeSitterNodes.add(node)
+ })
+
+ function getTreeSitterNodesByLocationAndHash(from, to, hash) {
+ return treeSitterNodesByLocationAndHash.get(from + "-" + to + "-" + hash)
+ }
+
+ let addedTreeSitterNodes = []
+ let removedDomainObjects = []
+
+ rootDomainObject.visit(domainObject => {
+ let adjustedStartIndex = this.adjustIndex(domainObject.treeSitter.startIndex, edit)
+ let adjustedEndIndex = this.adjustIndex(domainObject.treeSitter.endIndex, edit)
+ let hash = cyrb53(domainObject.treeSitter.text)
+
+ var correspondingNewTreeSitterNode = getTreeSitterNodesByLocationAndHash(adjustedStartIndex, adjustedEndIndex, hash)
+
+ if (correspondingNewTreeSitterNode) {
+ // take new correspondingNewTreeSitterNode as your own
+ remainingNewTreeSitterNodes.delete(correspondingNewTreeSitterNode)
+ } else {
+ removedDomainObjects.push(domainObject)
+ }
+ })
+
+ for(let node of remainingNewTreeSitterNodes) {
+ // create new domain object and put at right position
+ // var newDomainObject = TreeSitterDomainObject.fromTreeSitterAST()
+ }*/
+
+
+ }
printStructure() {
return "(" + this.type + this.children
@@ -204,13 +331,15 @@ export class TreeSitterDomainObject extends DomainObject {
var newAST = TreeSitterDomainObject.parser.parse(livelyCodeMirror.value, this.treeSitter.tree);
this.debugNewAST = newAST
- DomainObject.updateFromTreeSitter(this.rootNode(), newAST.rootNode)
+ DomainObject.updateFromTreeSitter(this.rootNode(), newAST.rootNode, edit)
livelyCodeMirror.dispatchEvent(new CustomEvent("domain-code-changed", {detail: {node: this, edit: edit}}))
}
-
+ get isTreeSitter() {
+ return true
+ }
get startPosition() {
return this.treeSitter.startPosition
@@ -243,29 +372,16 @@ export class TreeSitterDomainObject extends DomainObject {
return this.fromTreeSitterAST(ast.rootNode)
}
- static fromTreeSitterAST(ast, optionalDomainObjectsById, optionalUsedDomainObjects) {
- let domainObject
-
- if (optionalDomainObjectsById) {
- domainObject = optionalDomainObjectsById.get(ast.id)
-
- if (domainObject) {
- domainObject.treeSitter = ast
- if (optionalUsedDomainObjects) optionalUsedDomainObjects.add(domainObject)
- }
- }
- if (!domainObject) {
- domainObject = new TreeSitterDomainObject(ast)
- }
+ static fromTreeSitterAST(ast) {
+ let domainObject = new TreeSitterDomainObject(ast)
domainObject.children = []
for(var i=0; i < ast.childCount; i++) {
var child = ast.child(i)
- let domainChild = TreeSitterDomainObject.fromTreeSitterAST(child, optionalDomainObjectsById, optionalUsedDomainObjects)
+ let domainChild = TreeSitterDomainObject.fromTreeSitterAST(child)
domainChild.parent = domainObject
domainObject.children.push(domainChild)
}
-
return domainObject
}
}
diff --git a/src/client/media/Falleri2014FGA_algorithm2.png b/src/client/media/Falleri2014FGA_algorithm2.png
new file mode 100644
index 000000000..86befe3c4
Binary files /dev/null and b/src/client/media/Falleri2014FGA_algorithm2.png differ
diff --git a/src/client/media/Falleri2014FGA_alorighm1.png b/src/client/media/Falleri2014FGA_alorighm1.png
new file mode 100644
index 000000000..e1fd35abc
Binary files /dev/null and b/src/client/media/Falleri2014FGA_alorighm1.png differ
diff --git a/src/client/tree-sitter.js b/src/client/tree-sitter.js
index b67e8c13e..0f7572d87 100644
--- a/src/client/tree-sitter.js
+++ b/src/client/tree-sitter.js
@@ -1,13 +1,260 @@
+/*MD
+[test](edit://test/tree-sitter-test.js)
+
+MD*/
+import PriorityQueue from "src/external/priority-queue.js"
+import _ from 'src/external/lodash/lodash.js'
+
-// #Copy from /src/components/tools/lively-ast-treesitter-inspector.js
-// #TODO extract... ?
await lively.loadJavaScriptThroughDOM("treeSitter", lively4url + "/src/external/tree-sitter/tree-sitter.js")
export const Parser = window.TreeSitter;
await Parser.init()
-export const JavaScript = await Parser.Language.load(lively4url + "/src/external/tree-sitter/tree-sitter-javascript.wasm");
+export const JavaScript = await Parser.Language.load(lively4url +
+ "/src/external/tree-sitter/tree-sitter-javascript.wasm");
+
+
+export function visit(node, func) {
+ func(node)
+ for (let i = 0; i < node.childCount; i++) {
+ let ea = node.child(i)
+ visit(ea, func)
+ }
+}
+
+export function visitPostorder(node, func) {
+ for (let i = 0; i < node.childCount; i++) {
+ let ea = node.child(i)
+ visitPostorder(ea, func)
+ }
+ func(node)
+}
+
+
+/*MD SOURCE: Falleri 2014. Fine-grained and Accurate Source Code Differencing MD*/
+
+function root(node) {
+ return node.tree.rootNode
+}
+
+function peekMax(priorityList) {
+ return priorityList.peek().node
+}
+
+function pop(priorityList) {
+ return priorityList.pop().node
+}
+
+
+// Define a function to check if two subtrees are isomorphic
+export function isomorphic(node1, node2) {
+ if (!node1 && !node2) {
+ // Both nodes are null, they are isomorphic
+ return true;
+ }
+
+ if (!node1 || !node2) {
+ // One of the nodes is null, they are not isomorphic
+ return false;
+ }
+
+ if (node1.type !== node2.type) {
+ // Node types are different, they are not isomorphic
+ return false;
+ }
+
+ // Recursively check the child nodes
+ const children1 = node1.children || [];
+ const children2 = node2.children || [];
+
+ if (children1.length !== children2.length) {
+ // Different number of child nodes, they are not isomorphic
+ return false;
+ }
+
+ for (let i = 0; i < children1.length; i++) {
+ if (!isomorphic(children1[i], children2[i])) {
+ // Child subtrees are not isomorphic, so the subtrees are not isomorphic
+ return false;
+ }
+ }
+
+ if (children1.length === 0) {
+ return label(node1) === label(node2)
+ }
+
+ return true;
+}
+
+function dice(t1, t2, M) {
+ // Extract elements of s(t1) that are mapped with t2 in M
+ const mappedElements = Array.from(s(t1)).filter(t => M.has([t, t2]));
+
+ // Return the Dice coefficient
+ return 2 * mappedElements.length / (s(t1).length + s(t2).length);
+}
+
+function s(node) {
+ // set of decendents of node
+ var result = new Set()
+ visit(node, ea => result.add(ea))
+ result.delete(node) // not myself
+ return result
+}
+
+function open(node, priorityList) {
+ for(let ea of node.children) {
+ priorityList.push({height: height(ea), node: ea})
+ }
+}
+
+
+/*MD ![](media/Falleri2014FGA_alorighm1.png){width=400px} MD*/
+function height(node) {
+ /* "The height of a node t ∈ T is defined as:
+ 1) for a leaf node t, height(t) = 1 and
+ 2) for an internal node t, height(t) = max({height(c)|c ∈ children(t)}) + 1."
+ */
+
+ if (node.childCount === 0) return 1
+
+ return _.max(node.children.map(ea => height(ea)))
+}
+
+export function mapTrees(T1, T2, minHeight) {
+ let L1 = new PriorityQueue((a, b) => a.height > b.height),
+ L2 = new PriorityQueue((a, b) => a.height > b.height),
+ A = [],
+ M = new Set();
+
+ L1.push({ height: height(T1), node: T1 });
+ L2.push({ height: height(T2), node: T2 });
+
+ while (Math.min(peekMax(L1), peekMax(L2)) > minHeight) {
+ if (peekMax(L1) !== peekMax(L2)) {
+ if (peekMax(L1) > peekMax(L2)) {
+ for (let t of pop(L1)) {
+ open(t, L1);
+ }
+ } else {
+ for (let t of pop(L2)) {
+ open(t, L2);
+ }
+ }
+ } else {
+ const H1 = pop(L1);
+ const H2 = pop(L2);
+
+ for (let t1 of H1) {
+ for (let t2 of H2) {
+ if (isomorphic(t1, t2)) {
+ const existTxT2 = T2.some(tx => isomorphic(t1, tx) && tx !== t2);
+ const existTxT1 = T1.some(tx => isomorphic(tx, t2) && tx !== t1);
+
+ if (existTxT2 || existTxT1) {
+ A.push([t1, t2]);
+ } else {
+ // Add pairs of isomorphic nodes of s(t1) and s(t2) to M
+ // You'll need to define what "s(t1)" refers to in your actual code
+ }
+ }
+ }
+ }
+
+ for (let t1 of H1) {
+ if (!A.some(pair => pair[0] === t1) && !M.has(t1)) {
+ open(t1, L1);
+ }
+ }
+
+ for (let t2 of H2) {
+ if (!A.some(pair => pair[1] === t2) && !M.has(t2)) {
+ open(t2, L2);
+ }
+ }
+ }
+ }
+
+ A.sort((a, b) => dice(parent(a[0]), parent(b[0]), M));
+
+ while (A.length > 0) {
+ const [t1, t2] = A.shift();
+
+ // Add pairs of isomorphic nodes of s(t1) and s(t2) to M
+ // Again, you'll need to define what "s(t1)" refers to
+
+ A = A.filter(pair => pair[0] !== t1);
+ A = A.filter(pair => pair[1] !== t2);
+ }
+
+ return M;
+}
+// Helper functions (peekMax, open, root, isomorphic, etc.) will need to be defined.
+
+
+function candidate(t, M) {
+ /* "For each unmatched non-leaf node of T1, we extract a list of candidate nodes from T2. A node c ∈ T2 is a candidate for t1 if label(t1) = label(c), c is unmatched, and t1 and c have some matching descendants. We then select the candidate t2 ∈ T2 with the greatest dice(t1, t2,M) value. If dice(t1, t2,M) > minDice, t1 and t2 are matched together." [Falleri2014FGA] */
+}
+
+/*MD ![](media/Falleri2014FGA_algorithm2.png){width=400px} MD*/
+function isMatched(node, M) {
+ // TODO
+}
+
+function hasMatchedChildren(t1, M) {
+ // TODO
+}
+
+function opt(node1, node2) {
+ "finds a shortest edit script without move actions. In our implementation we use the RTED algorithm [27]. The mappings induced from this edit script are added in M if they involve nodes with identical labels."
+}
+
+function label(node) {
+ if (node.childCount === 0) {
+ return node.text
+ }
+ return node.type
+}
+
+function bottomUpPhase(T1, T2, M, minDice, maxSize) {
+
+ visitPostorder(T1, t1 => {
+ if (!isMatched(t1, M) && hasMatchedChildren(t1, M)) {
+ let t2 = candidate(t1, M);
+ if (t2 !== null && dice(t1, t2, M) > minDice) {
+ M.add([t1, t2]);
+ if (Math.max(s(t1).size, s(t2)).size < maxSize) {
+ let R = opt(t1, t2);
+ for (let [ta, tb] of R) {
+ if (!isMatched(ta, M) && !isMatched(tb, M) && label(ta) === label(tb)) {
+ M.add([ta, tb]);
+ }
+ }
+ }
+ }
+ }
+ })
+ return M;
+}
+
+// Helper functions like postOrder, isMatched, hasMatchedChildren, candidate, dice,
+// s, size, opt, and label will need to be defined based on your specific requirements.
+
+
+export function match(tree1, tree2) {
+ let matches = new Set();
+ // "We recommend minHeight = 2 to avoid single identifiers to match everywhere." [Falleri2014FGA]
+ let minHeight = 2
+ mapTrees(tree1, tree2, minHeight)
+ // "maxSize is used in the recovery part of Algorithm 2 that can trigger a cubic algorithm. To avoid long computation times we recommend to use maxSize = 100."[Falleri2014FGA]
+ let maxSize = 100
+ // "Finally under 50% of common nodes, two container nodes are probably different. Therefore we recommend using minDice = 0.5"
+ let minDice = 0.5
+ bottomUpPhase(tree1, tree2, matches, minDice, maxSize)
+ return Array.from(matches);
+}
diff --git a/src/components/tools/lively-ast-treesitter-inspector.js b/src/components/tools/lively-ast-treesitter-inspector.js
index a6540a96f..11749c875 100644
--- a/src/components/tools/lively-ast-treesitter-inspector.js
+++ b/src/components/tools/lively-ast-treesitter-inspector.js
@@ -88,7 +88,8 @@ export default class AstTreesitterInspector extends AstInspector {
element.append(this.keyTemplate(element));
element.append(this.labelTemplate(target.type));
element.append({target.id});
- element.append( {target.startIndex}-{target.endIndex} );
+ element.append( {target.id - target.tree.rootNode.id});
+ element.append( {target.startIndex}-{target.endIndex} );
element.append();
const summary = this.treeSitterNodeSummary(element.target, element.isExpanded);
diff --git a/src/components/tools/lively-domain-code-explorer-example-source.js b/src/components/tools/lively-domain-code-explorer-example-source.js
index 7de3ed6fc..52d00b079 100644
--- a/src/components/tools/lively-domain-code-explorer-example-source.js
+++ b/src/components/tools/lively-domain-code-explorer-example-source.js
@@ -1,3 +1,4 @@
let a = 3 + 4
const b = a
+
var c = b
diff --git a/src/components/tools/treesitter-matches.html b/src/components/tools/treesitter-matches.html
new file mode 100644
index 000000000..9e6ed6992
--- /dev/null
+++ b/src/components/tools/treesitter-matches.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/tools/treesitter-matches.js b/src/components/tools/treesitter-matches.js
new file mode 100644
index 000000000..bca7271be
--- /dev/null
+++ b/src/components/tools/treesitter-matches.js
@@ -0,0 +1,76 @@
+"enable aexpr";
+
+
+/*MD [Demo](browse://demos/tree-sitter/matches.md) MD*/
+
+import Morph from 'src/components/widgets/lively-morph.js';
+import {visit, Parser, JavaScript, match} from 'src/client/tree-sitter.js';
+
+export default class TreesitterMatches extends Morph {
+
+ async update() {
+ let graphviz = await ()
+
+ function renderTree(rootNode, clusterName) {
+ let dotEdges = []
+ let dotNodes = []
+
+ visit(rootNode, node => {
+ dotNodes.push(`${node.id}[label="${node.type}"]`)
+ if (node.parent) dotEdges.push(`${node.parent.id} -> ${node.id}`)
+ })
+
+ return `subgraph ${clusterName} {
+ ${dotNodes.join(";\n")}
+ ${dotEdges.join(";\n")}
+ }`
+
+ }
+
+ function renderMatches(matches) {
+ let dotEdges = []
+ for(let match of matches) {
+ dotEdges.push(`${match.node1.id} -> ${match.node2.id} [color=green]`)
+ }
+ return dotEdges.join(";\n")
+ }
+
+ var source = `digraph {
+ rankdir=TB;
+ graph [
+ splines="true"
+ overlap="false" ];
+ node [ style="solid" shape="plain" fontname="Arial" fontsize="14" fontcolor="black" ];
+ edge [ fontname="Arial" fontsize="8" ];
+ ${renderTree(this.tree1.rootNode, "cluster_0")}
+ ${renderTree(this.tree2.rootNode, "cluster_1")}
+ ${renderMatches(this.matches)}
+ }`
+ graphviz.innerHTML = `<` +`script type="graphviz">${source}<` + `/script>}`
+
+ graphviz.updateViz()
+
+ this.get("#pane").innerHTML = ""
+ this.get("#pane").appendChild(graphviz)
+ }
+
+
+ async livelyExample() {
+
+
+ var parser = new Parser();
+ parser.setLanguage(JavaScript);
+
+ let sourceCode1 = `let a = 3 + 4`
+ this.tree1 = parser.parse(sourceCode1);
+
+ let sourceCode2 = `let a = 3 + 4\na++`
+ this.tree2 = parser.parse(sourceCode2);
+
+ this.matches = match(this.tree1.rootNode, this.tree2.rootNode)
+
+ this.update()
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/external/priority-queue.js b/src/external/priority-queue.js
new file mode 100644
index 000000000..ce1ec7b2d
--- /dev/null
+++ b/src/external/priority-queue.js
@@ -0,0 +1,95 @@
+// source: https://stackoverflow.com/questions/42919469/efficient-way-to-implement-priority-queue-in-javascript
+
+/*MD
+## Example
+```javascript {style="background:lightgray"}
+import PriorityQueue from "src/external/priority-queue.js"
+
+
+// Default comparison semantics
+const queue = new PriorityQueue();
+queue.push(10, 20, 30, 40, 50);
+console.log('Top:', queue.peek()); //=> 50
+console.log('Size:', queue.size()); //=> 5
+console.log('Contents:');
+while (!queue.isEmpty()) {
+ console.log(queue.pop()); //=> 40, 30, 20, 10
+}
+
+// Pairwise comparison semantics
+const pairwiseQueue = new PriorityQueue((a, b) => a[1] > b[1]);
+pairwiseQueue.push(['low', 0], ['medium', 5], ['high', 10]);
+console.log('\nContents:');
+while (!pairwiseQueue.isEmpty()) {
+ console.log(pairwiseQueue.pop()[0]); //=> 'high', 'medium', 'low'
+}
+```
+MD*/
+
+const top = 0;
+const parent = i => ((i + 1) >>> 1) - 1;
+const left = i => (i << 1) + 1;
+const right = i => (i + 1) << 1;
+
+export default class PriorityQueue {
+ constructor(comparator = (a, b) => a > b) {
+ this._heap = [];
+ this._comparator = comparator;
+ }
+ size() {
+ return this._heap.length;
+ }
+ isEmpty() {
+ return this.size() == 0;
+ }
+ peek() {
+ return this._heap[top];
+ }
+ push(...values) {
+ values.forEach(value => {
+ this._heap.push(value);
+ this._siftUp();
+ });
+ return this.size();
+ }
+ pop() {
+ const poppedValue = this.peek();
+ const bottom = this.size() - 1;
+ if (bottom > top) {
+ this._swap(top, bottom);
+ }
+ this._heap.pop();
+ this._siftDown();
+ return poppedValue;
+ }
+ replace(value) {
+ const replacedValue = this.peek();
+ this._heap[top] = value;
+ this._siftDown();
+ return replacedValue;
+ }
+ _greater(i, j) {
+ return this._comparator(this._heap[i], this._heap[j]);
+ }
+ _swap(i, j) {
+ [this._heap[i], this._heap[j]] = [this._heap[j], this._heap[i]];
+ }
+ _siftUp() {
+ let node = this.size() - 1;
+ while (node > top && this._greater(node, parent(node))) {
+ this._swap(node, parent(node));
+ node = parent(node);
+ }
+ }
+ _siftDown() {
+ let node = top;
+ while (
+ (left(node) < this.size() && this._greater(left(node), node)) ||
+ (right(node) < this.size() && this._greater(right(node), node))
+ ) {
+ let maxChild = (right(node) < this.size() && this._greater(right(node), left(node))) ? right(node) : left(node);
+ this._swap(node, maxChild);
+ node = maxChild;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/domain-code-test.js b/test/domain-code-test.js
index 1b646f201..a7f7a1421 100644
--- a/test/domain-code-test.js
+++ b/test/domain-code-test.js
@@ -50,13 +50,15 @@ describe('TreeSitter', () => {
startPosition: {row: 1, column: 0},
oldEndPosition: {row: 1, column: 5},
newEndPosition: {row: 1, column: 3},
+
}
- originalAST.edit(edit);
- treesitterVisit(originalAST.rootNode, node => node.edit(edit)) // to update index
+ window.xoriginalAST = originalAST
- var newAST = TreeSitterDomainObject.parser.parse(newSourceCode, originalAST);
+ var result = originalAST.edit(edit);
+ debugger
+ // treesitterVisit(originalAST.rootNode, node => node.edit(edit)) // to update index
- window.xoriginalAST = originalAST
+ var newAST = TreeSitterDomainObject.parser.parse(newSourceCode, originalAST);
window.xnewAST = newAST
@@ -80,6 +82,92 @@ describe('Domain Code', () => {
});
describe('DomainObject', () => {
+ it('reconciles change when adding new statement at start', () => {
+ let sourceOriginal = `
+a = 3`
+ let sourceNew = `l
+a = 3`
+ let root = TreeSitterDomainObject.fromSource(sourceOriginal)
+ DomainObject.edit(root, sourceNew, { startIndex: 0, oldEndIndex: 0, newEndIndex: 1 })
+
+ expect(root.children.length).equals(2);
+ expect(root.children[1].children[0].type).equals("assignment_expression")
+ })
+
+ it('reconciles change when adding new statement at end', () => {
+ let sourceOriginal = `a = 3`
+ let sourceNew = `a = 3
+l`
+ let root = TreeSitterDomainObject.fromSource(sourceOriginal)
+ DomainObject.edit(root, sourceNew, { startIndex: 0, oldEndIndex: 0, newEndIndex: 1 })
+
+ expect(root.children.length).equals(2);
+ expect(root.children[0].children[0].type).equals("assignment_expression")
+ })
+
+ it('reconciles change when removing statement at end', () => {
+ let sourceOriginal = `a = 3
+l`
+ let sourceNew = `a = 3`
+ let root = TreeSitterDomainObject.fromSource(sourceOriginal)
+ DomainObject.edit(root, sourceNew, { startIndex: 0, oldEndIndex: 0, newEndIndex: 1 })
+
+ expect(root.children.length).equals(1);
+ expect(root.children[0].children[0].type).equals("assignment_expression")
+ })
+
+ it('reconciles change when removing statement at start', () => {
+ let sourceOriginal = `l
+a = 3`
+ let sourceNew = `a = 3`
+ let root = TreeSitterDomainObject.fromSource(sourceOriginal)
+ DomainObject.edit(root, sourceNew, { startIndex: 0, oldEndIndex: 0, newEndIndex: 1 })
+
+ expect(root.children.length).equals(1);
+ expect(root.children[0].children[0].type).equals("assignment_expression")
+ })
+
+ it('reconciles change when adding new statement at start of a function', () => {
+ let sourceOriginal = `function() {
+
+ let a = 3
+}`
+ let sourceNew = `function() {
+ l
+ let a = 3
+}`
+ let root = TreeSitterDomainObject.fromSource(sourceOriginal)
+ DomainObject.edit(root, sourceNew, { startIndex: 15, oldEndIndex: 15, newEndIndex: 16 })
+
+ const block = root.children[0].children[0].children[2]
+ expect(block.type).equals("statement_block")
+
+ expect(block.children.length).equals(4);
+ expect(block.children[2].type).equals("lexical_declaration")
+ })
+
+ describe('adjustIndex', () => {
+ it('do nothing to index before edits', async () => {
+ var index = 3
+ var newIndex = DomainObject.adjustIndex(index, {startIndex: 5, oldEndIndex: 5, newEndIndex: 6})
+ expect(newIndex).equals(index)
+ })
+
+ it('increases for index after adding edits', async () => {
+ var index = 10
+ var newIndex = DomainObject.adjustIndex(index, {startIndex: 5, oldEndIndex: 5, newEndIndex: 6})
+ expect(newIndex).equals(11)
+ })
+
+ it('decreases for index after deleting edits', async () => {
+ var index = 10
+ var newIndex = DomainObject.adjustIndex(index, {startIndex: 5, oldEndIndex: 5, newEndIndex: 4})
+ expect(newIndex).equals(9)
+ })
+
+
+ })
+
describe('updateFromTreeSitter', () => {
it('should update let to const', async () => {
let sourceCode = `let a = 3 + 4\nconsole.log("x")`
diff --git a/test/tree-sitter-test.js b/test/tree-sitter-test.js
new file mode 100644
index 000000000..049caafa3
--- /dev/null
+++ b/test/tree-sitter-test.js
@@ -0,0 +1,63 @@
+import {expect} from 'src/external/chai.js';
+
+
+
+import {Parser, JavaScript, match, isomorphic} from 'src/client/tree-sitter.js';
+
+
+var parser = new Parser();
+parser.setLanguage(JavaScript);
+
+function parseAll(sources) {
+ return sources.map(ea => parser.parse(ea))
+}
+
+describe('tree-sitter', () => {
+ describe('isomorphic', () => {
+ it('find identical trees', async () => {
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`, `let a = 3 + 4`])
+ expect(isomorphic(tree1.rootNode, tree2.rootNode)).to.be.true
+ })
+
+ it('find identical trees with added whitespace', async () => {
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`, ` let
+a = 3 + 4`])
+ expect(isomorphic(tree1.rootNode, tree2.rootNode)).to.be.true
+ })
+
+ it('find different trees', async () => {
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`, `let a = 3 + 5`])
+ expect(isomorphic(tree1.rootNode, tree2.rootNode)).to.be.false
+ })
+
+ it('find different structured trees', async () => {
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`, `let a = 3 + 4; let b`])
+ expect(isomorphic(tree1.rootNode, tree2.rootNode)).to.be.false
+ })
+ })
+
+ describe('match', () => {
+
+ it('match identical trees', async () => {
+
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`,`let a = 3 + 4`])
+ var matches = match(tree1.rootNode, tree2.rootNode)
+
+ expect(matches.length).gt(5)
+ for(let match of matches) {
+ expect(match.node1.text).to.equal(match.node2.text)
+ }
+ })
+
+ it('should match unganged trees', async () => {
+ let [tree1, tree2] = parseAll([`let a = 3 + 4`, `let a = 3 + 4\na++`])
+ var matches = match(tree1.rootNode, tree2.rootNode)
+ expect(matches.length).gt(5)
+ for(let match of matches) {
+ if (match.node1.text !== tree1.rootNode.text) {
+ expect(match.node1.text).to.equal(match.node2.text)
+ }
+ }
+ })
+ })
+})
\ No newline at end of file