diff --git a/demos/tree-sitter/matches.md b/demos/tree-sitter/matches.md index 1d3e5973c..e9ed8d317 100644 --- a/demos/tree-sitter/matches.md +++ b/demos/tree-sitter/matches.md @@ -13,9 +13,18 @@ var vis = await () // editor1.value = `let a = 3 + 4` - editor1.value = `let a = 3` + editor1.value = `class Test { + foo(i) { + if (i == 0) return "Foo!" + } +}` // editor2.value = `let a = 3 + 4\na++` - editor2.value = `{let a = 2+4}` + editor2.value = `class Test { + foo(i) { + if (i == 0) return "Bar" + else if (i == -1) return "Foo!" + } +}` editor1.editor.on("change", (() => update()).debounce(500)); editor2.editor.on("change", (() => update()).debounce(500)); diff --git a/src/client/domain-code.js b/src/client/domain-code.js index 38ffcd20f..d7c7d2391 100644 --- a/src/client/domain-code.js +++ b/src/client/domain-code.js @@ -12,10 +12,14 @@ MD*/ import tinycolor from 'src/external/tinycolor.js'; -import {Parser, JavaScript, visit as treeSitterVisit} from "src/client/tree-sitter.js" +import {Parser, JavaScript, visit as treeSitterVisit, match} from "src/client/tree-sitter.js" import {loc} from "utils" + +import { ChawatheScriptGenerator} from 'src/client/domain-code/chawathe-script-generator.js'; + + // 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; @@ -162,99 +166,23 @@ export class DomainObject { let { startIndex, oldEndIndex, newEndIndex } = edit - // 1. detect editit history (diff oldTree -> newTree) - // a) deleted nodes from oldTree - // b) added nodes in new tree + let originalAST = rootDomainObject.treeSitter.tree + let originalSource = originalAST.rootNode.text - // 2. apply diff to domain tree + let newAST = TreeSitterDomainObject.fromSource(originalSource) - - 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] + if(!originalAST) {throw new Error("originalAST missing")} + if(!newAST) {throw new Error("originalAST missing")} - 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 = [] + debugger + let mappings = match(originalAST.rootNode, newAST.rootNode, 0, 100) + var scriptGenerator = new ChawatheScriptGenerator() + scriptGenerator.initWith(originalAST.rootNode, newAST.rootNode, mappings) - 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) - } - }) + scriptGenerator.generate() - for(let node of remainingNewTreeSitterNodes) { - // create new domain object and put at right position - // var newDomainObject = TreeSitterDomainObject.fromTreeSitterAST() - }*/ - } printStructure() { diff --git a/src/client/tree-sitter.js b/src/client/tree-sitter.js index 743e672b6..74fbdba16 100644 --- a/src/client/tree-sitter.js +++ b/src/client/tree-sitter.js @@ -222,7 +222,7 @@ export function mapTrees(T1, T2, minHeight) { if (existTxT2 || existTxT1) { candidateMappings.push([t1, t2]); } else { - visitPairs(t1, t2, (node1, node2) => addMapping(mappings, node1, node2)) + visitPairs(t1, t2, (node1, node2) => addMapping(mappings, node1, node2, {phase: "mapTrees_01"})) } } } @@ -247,7 +247,7 @@ export function mapTrees(T1, T2, minHeight) { while (candidateMappings.length > 0) { const [t1, t2] = candidateMappings.shift(); - visitPairs(t1, t2, (node1, node2) => addMapping(mappings, node1, node2)) + visitPairs(t1, t2, (node1, node2) => addMapping(mappings, node1, node2, {phase:"mapTrees_02"})) candidateMappings = candidateMappings.filter(pair => pair[0] !== t1); candidateMappings = candidateMappings.filter(pair => pair[1] !== t2); @@ -334,6 +334,7 @@ function isLeaf(node) { function lastChanceMatch(mappings, src, dst, maxSize) { if (s(src).size < maxSize || s(dst).size < maxSize) { + var debugStartTime = performance.now() let zsMappings = zhangShashaMapping(src, dst, function children(node) { return node.children }, function insertCost() { return 1 }, @@ -345,10 +346,14 @@ function lastChanceMatch(mappings, src, dst, maxSize) { return 1 } }); + debugLastChanceCounter++ + var debugTime = performance.now() - debugStartTime + for (let candidate of zsMappings) { if (candidate.t1 && candidate.t2) { if (!isSrcMapped(mappings, candidate.t1) && !isDstMapped(mappings, candidate.t2)) { - addMapping(mappings, candidate.t1, candidate.t2); + addMapping(mappings, candidate.t1, candidate.t2, + {phase: "lastChanceMatch", lastChanceCounter: debugLastChanceCounter, time: debugTime}); } } } @@ -362,7 +367,7 @@ export function hasMapping(mappings, t1, t2) { return mappings.find(ea => ea.node2.id == t1.id && ea.node2.id == t2.id) } -export function addMapping(mappings, t1, t2) { +export function addMapping(mappings, t1, t2, debugInfo) { if (!t1) { throw new Error("t1 is null") } if (!t2) { throw new Error("t2 is null") } @@ -371,15 +376,18 @@ export function addMapping(mappings, t1, t2) { debugger throw new Error("mapping gone wrong?") } - mappings.push({ node1: t1, node2: t2 }) + mappings.push({ node1: t1, node2: t2, debugInfo: debugInfo}) } -function bottomUpPhase(T1, dst, mappings, minDice, maxSize) { +var debugLastChanceCounter = 0 +function bottomUpPhase(T1, dst, mappings, minDice, maxSize) { + debugLastChanceCounter = 0 + visitPostorder(T1, t => { if (!t.parent) { if (!isSrcMapped(mappings, t)) { - addMapping(mappings, t, dst) + addMapping(mappings, t, dst, {phase: "bottomUpRoot"}) lastChanceMatch(mappings, t, dst, maxSize); } } else if (!isSrcMapped(mappings, t) && !isLeaf(t)) { @@ -396,7 +404,7 @@ function bottomUpPhase(T1, dst, mappings, minDice, maxSize) { if (best !== null) { lastChanceMatch(mappings, t, best, maxSize); - addMapping(mappings, t, best) + addMapping(mappings, t, best, {phase: "bottomUp"}) } } }) @@ -412,6 +420,7 @@ export function match(tree1, tree2, minHeight = 2, maxSize = 100, minDice=0.5) { let matches = mapTrees(tree1, tree2, minHeight) + bottomUpPhase(tree1, tree2, matches, minDice, maxSize) return Array.from(matches); diff --git a/src/components/tools/treesitter-matches.js b/src/components/tools/treesitter-matches.js index 0b74e20b7..51060c078 100644 --- a/src/components/tools/treesitter-matches.js +++ b/src/components/tools/treesitter-matches.js @@ -11,7 +11,7 @@ export default class TreesitterMatches extends Morph { get livelyUpdateStrategy() { return 'inplace'; } async update() { - let graphviz = await () + let graphviz = await () function renderTree(rootNode, clusterName) { let dotEdges = [] @@ -28,11 +28,51 @@ export default class TreesitterMatches extends Morph { }` } + + function colorForPhase(phase) { + var colors = { + mapTrees_01: "green", + mapTrees_02: "green", + lastChanceMatch: "blue", + bottomUp: "red", + bottomUpRoot: "orange" + } + + if (!colors[phase]) { + debugger + } + + return colors[phase] || "gray" + } + + function labelFor(match) { + if (match.debugInfo && match.debugInfo.phase === "lastChanceMatch") { + return match.debugInfo.lastChanceCounter + } + return "" + } + + function tooltipFor(match) { + if (!match.debugInfo) return "" + var s = match.debugInfo.phase + if (match.debugInfo.time) { + s += " " + match.debugInfo.time +"ms" + } + return s + } + + function widthFor(match) { + if (match.debugInfo && match.debugInfo.time) { + return match.debugInfo.time * 1 + } + return 1 + } + function renderMatches(matches) { let dotEdges = [] for(let match of matches) { - dotEdges.push(`${match.node1.id} -> ${match.node2.id} [color=green]`) + dotEdges.push(`${match.node1.id} -> ${match.node2.id} [color="${match.debugInfo ? colorForPhase(match.debugInfo.phase) : "gray"}" penwidth="${widthFor(match)}" tooltip="${tooltipFor(match)}" label="${labelFor(match)}"]`) } return dotEdges.join(";\n") } @@ -63,10 +103,19 @@ export default class TreesitterMatches extends Morph { var parser = new Parser(); parser.setLanguage(JavaScript); - let sourceCode1 = `let a = 3 + 4` + let sourceCode1 = `class Test { + foo(i) { + if (i == 0) return "Foo!" + } +}` this.tree1 = parser.parse(sourceCode1); - let sourceCode2 = `let a = 3 + 4\na++` + let sourceCode2 = `class Test { + foo(i) { + if (i == 0) return "Bar" + else if (i == -1) return "Foo!" + } +}` this.tree2 = parser.parse(sourceCode2); this.matches = match(this.tree1.rootNode, this.tree2.rootNode) diff --git a/test/domain-code-test.js b/test/domain-code-test.js index 7fb96e967..412abb757 100644 --- a/test/domain-code-test.js +++ b/test/domain-code-test.js @@ -105,7 +105,7 @@ l` expect(root.children[0].children[0].type).equals("assignment_expression") }) - xit('reconciles change when removing statement at end', () => { + it('reconciles change when removing statement at end', () => { let sourceOriginal = `a = 3 l` let sourceNew = `a = 3`