Skip to content

Commit

Permalink
added more details in treesitter match visualization
Browse files Browse the repository at this point in the history
SQUASHED: AUTO-COMMIT-demos-tree-sitter-matches.md,AUTO-COMMIT-src-client-domain-code.js,AUTO-COMMIT-src-client-tree-sitter.js,AUTO-COMMIT-src-components-tools-treesitter-matches.js,AUTO-COMMIT-test-domain-code-test.js,
  • Loading branch information
JensLincke committed Oct 13, 2023
1 parent 7df1c69 commit 9c28a74
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 102 deletions.
13 changes: 11 additions & 2 deletions demos/tree-sitter/matches.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@
var vis = await (<treesitter-matches></treesitter-matches>)

// 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));
Expand Down
102 changes: 15 additions & 87 deletions src/client/domain-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
25 changes: 17 additions & 8 deletions src/client/tree-sitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"}))
}
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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 },
Expand All @@ -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});
}
}
}
Expand All @@ -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") }

Expand All @@ -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)) {
Expand All @@ -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"})
}
}
})
Expand All @@ -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);
Expand Down
57 changes: 53 additions & 4 deletions src/components/tools/treesitter-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class TreesitterMatches extends Morph {
get livelyUpdateStrategy() { return 'inplace'; }

async update() {
let graphviz = await (<graphviz-dot></graphviz-dot>)
let graphviz = await (<graphviz-dot style="width: 2000px"></graphviz-dot>)

function renderTree(rootNode, clusterName) {
let dotEdges = []
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/domain-code-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down

0 comments on commit 9c28a74

Please sign in to comment.