Render syntax tree efficiently in docs playground
This commit is contained in:
parent
3425b6e1c2
commit
73e4db41b0
5 changed files with 284 additions and 185 deletions
|
|
@ -144,15 +144,20 @@ body {
|
|||
}
|
||||
|
||||
#output-container {
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#output-container-scroll {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
overflow: auto;
|
||||
padding: 20px 10px;
|
||||
max-height: 350px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.tree-link.highlighted {
|
||||
a.highlighted {
|
||||
background-color: #ddd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,189 +1,269 @@
|
|||
const codeInput = document.getElementById('code-input');
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
const loggingCheckbox = document.getElementById('logging-checkbox');
|
||||
const outputContainer = document.getElementById('output-container');
|
||||
const updateTimeSpan = document.getElementById('update-time');
|
||||
const demoContainer = document.getElementById('playground-container');
|
||||
|
||||
const languagesByName = {};
|
||||
|
||||
let tree;
|
||||
let parser;
|
||||
let codeEditor;
|
||||
let queryEditor;
|
||||
let languageName;
|
||||
let highlightedNodeLink;
|
||||
|
||||
main();
|
||||
(async () => {
|
||||
const scriptURL = document.currentScript.getAttribute('src');
|
||||
const codeInput = document.getElementById('code-input');
|
||||
const languageSelect = document.getElementById('language-select');
|
||||
const loggingCheckbox = document.getElementById('logging-checkbox');
|
||||
const outputContainer = document.getElementById('output-container');
|
||||
const outputContainerScroll = document.getElementById('output-container-scroll');
|
||||
const updateTimeSpan = document.getElementById('update-time');
|
||||
const demoContainer = document.getElementById('playground-container');
|
||||
const languagesByName = {};
|
||||
|
||||
async function main() {
|
||||
await TreeSitter.init();
|
||||
parser = new TreeSitter();
|
||||
codeEditor = CodeMirror.fromTextArea(codeInput, {
|
||||
await Promise.all([
|
||||
codeInput.value = await fetch(scriptURL).then(r => r.text()),
|
||||
TreeSitter.init()
|
||||
]);
|
||||
|
||||
const parser = new TreeSitter();
|
||||
const codeEditor = CodeMirror.fromTextArea(codeInput, {
|
||||
lineNumbers: true,
|
||||
showCursorWhenSelecting: true
|
||||
});
|
||||
const cluster = new Clusterize({
|
||||
rows: [],
|
||||
noDataText: null,
|
||||
contentElem: outputContainer,
|
||||
scrollElem: outputContainerScroll
|
||||
});
|
||||
const renderTreeOnCodeChange = debounce(renderTree, 50);
|
||||
|
||||
let languageName = languageSelect.value;
|
||||
let treeRows = null;
|
||||
let treeRowHighlightedIndex = -1;
|
||||
let parseCount = 0;
|
||||
let isRendering = 0;
|
||||
|
||||
languageName = languageSelect.value;
|
||||
codeEditor.on('changes', handleCodeChange);
|
||||
codeEditor.on('cursorActivity', handleCursorMovement);
|
||||
codeEditor.on('cursorActivity', debounce(handleCursorMovement, 150));
|
||||
loggingCheckbox.addEventListener('change', handleLoggingChange);
|
||||
languageSelect.addEventListener('change', handleLanguageChange);
|
||||
outputContainer.addEventListener('click', handleTreeClick);
|
||||
|
||||
await handleLanguageChange();
|
||||
await handleLanguageChange()
|
||||
|
||||
demoContainer.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
async function handleLanguageChange() {
|
||||
const newLanguageName = languageSelect.value;
|
||||
if (!languagesByName[newLanguageName]) {
|
||||
const url = `${LANGUAGE_BASE_URL}/tree-sitter-${newLanguageName}.wasm`
|
||||
languageSelect.disabled = true;
|
||||
try {
|
||||
languagesByName[newLanguageName] = await TreeSitter.Language.load(url);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
languageSelect.value = languageName;
|
||||
return
|
||||
} finally {
|
||||
languageSelect.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
tree = null;
|
||||
languageName = newLanguageName;
|
||||
parser.setLanguage(languagesByName[newLanguageName]);
|
||||
handleCodeChange();
|
||||
}
|
||||
|
||||
function handleLoggingChange() {
|
||||
if (loggingCheckbox.checked) {
|
||||
parser.setLogger(console.log);
|
||||
} else {
|
||||
parser.setLogger(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCodeChange(editor, changes) {
|
||||
let start;
|
||||
if (tree && changes) {
|
||||
start = performance.now();
|
||||
for (const change of changes) {
|
||||
const edit = treeEditForEditorChange(change);
|
||||
tree.edit(edit);
|
||||
}
|
||||
} else {
|
||||
start = performance.now();
|
||||
}
|
||||
const newTree = parser.parse(codeEditor.getValue() + '\n', tree);
|
||||
tree && tree.delete();
|
||||
tree = newTree;
|
||||
updateTimeSpan.innerText = `${(performance.now() - start).toFixed(1)} ms`;
|
||||
renderTree();
|
||||
}
|
||||
|
||||
function handleTreeClick(event) {
|
||||
if (event.target.className === 'tree-link') {
|
||||
event.preventDefault();
|
||||
const row = parseInt(event.target.dataset.row);
|
||||
const column = parseInt(event.target.dataset.column);
|
||||
codeEditor.setCursor({line: row, ch: column});
|
||||
codeEditor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function treeEditForEditorChange(change) {
|
||||
const oldLineCount = change.removed.length;
|
||||
const newLineCount = change.text.length;
|
||||
const lastLineLength = change.text[newLineCount - 1].length;
|
||||
|
||||
const startPosition = {row: change.from.line, column: change.from.ch};
|
||||
const oldEndPosition = {row: change.to.line, column: change.to.ch};
|
||||
const newEndPosition = {
|
||||
row: startPosition.row + newLineCount - 1,
|
||||
column: newLineCount === 1
|
||||
? startPosition.column + lastLineLength
|
||||
: lastLineLength
|
||||
};
|
||||
|
||||
const startIndex = codeEditor.indexFromPos(change.from);
|
||||
let newEndIndex = startIndex + newLineCount - 1;
|
||||
let oldEndIndex = startIndex + oldLineCount - 1;
|
||||
for (let i = 0; i < newLineCount; i++) newEndIndex += change.text[i].length;
|
||||
for (let i = 0; i < oldLineCount; i++) oldEndIndex += change.removed[i].length;
|
||||
|
||||
return {
|
||||
startIndex, oldEndIndex, newEndIndex,
|
||||
startPosition, oldEndPosition, newEndPosition
|
||||
};
|
||||
}
|
||||
|
||||
var handleCursorMovement = debounce(() => {
|
||||
if (highlightedNodeLink) {
|
||||
highlightedNodeLink.classList.remove('highlighted');
|
||||
highlightedNodeLink = null;
|
||||
}
|
||||
|
||||
const selection = codeEditor.getDoc().listSelections()[0];
|
||||
let start = {row: selection.anchor.line, column: selection.anchor.ch};
|
||||
let end = {row: selection.head.line, column: selection.head.ch};
|
||||
if (
|
||||
start.row > end.row ||
|
||||
(
|
||||
start.row === end.row &&
|
||||
start.column > end.column
|
||||
)
|
||||
) {
|
||||
let swap = end;
|
||||
end = start;
|
||||
start = swap;
|
||||
}
|
||||
const node = tree.rootNode.namedDescendantForPosition(start, end);
|
||||
const link = document.querySelector(`.tree-link[data-id="${node[0]}"]`);
|
||||
link.classList.add('highlighted');
|
||||
highlightedNodeLink = link;
|
||||
|
||||
$(outputContainer).animate({
|
||||
scrollTop: Math.max(0, link.offsetTop - outputContainer.clientHeight / 2)
|
||||
}, 200);
|
||||
}, 300);
|
||||
|
||||
var renderTree = debounce(() => {
|
||||
let result = "";
|
||||
renderNode(tree.rootNode, 0);
|
||||
function renderNode(node, indentLevel) {
|
||||
let space = ' '.repeat(indentLevel);
|
||||
const type = node.type;
|
||||
const start = node.startPosition;
|
||||
const end = node.endPosition;
|
||||
result += space;
|
||||
result += "(<a class='tree-link' href='#' ";
|
||||
result += `data-id="${node[0]}" data-row=${start.row} data-column=${start.column}>${type}</a>`
|
||||
result += `[${start.row + 1}, ${start.column}] - [${end.row + 1}, ${end.column}]`;
|
||||
if (node.namedChildren.length > 0) {
|
||||
for (let i = 0, n = node.namedChildren.length; i < n; i++) {
|
||||
result += '\n';
|
||||
renderNode(node.namedChildren[i], indentLevel + 1);
|
||||
async function handleLanguageChange() {
|
||||
const newLanguageName = languageSelect.value;
|
||||
if (!languagesByName[newLanguageName]) {
|
||||
const url = `${LANGUAGE_BASE_URL}/tree-sitter-${newLanguageName}.wasm`
|
||||
languageSelect.disabled = true;
|
||||
try {
|
||||
languagesByName[newLanguageName] = await TreeSitter.Language.load(url);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
languageSelect.value = languageName;
|
||||
return
|
||||
} finally {
|
||||
languageSelect.disabled = false;
|
||||
}
|
||||
}
|
||||
result += ')';
|
||||
|
||||
tree = null;
|
||||
languageName = newLanguageName;
|
||||
parser.setLanguage(languagesByName[newLanguageName]);
|
||||
handleCodeChange();
|
||||
}
|
||||
|
||||
outputContainer.innerHTML = result;
|
||||
}, 200);
|
||||
async function handleCodeChange(editor, changes) {
|
||||
let start;
|
||||
if (tree && changes) {
|
||||
start = performance.now();
|
||||
for (const change of changes) {
|
||||
const edit = treeEditForEditorChange(change);
|
||||
tree.edit(edit);
|
||||
}
|
||||
} else {
|
||||
start = performance.now();
|
||||
}
|
||||
const newTree = parser.parse(codeEditor.getValue() + '\n', tree);
|
||||
const duration = (performance.now() - start).toFixed(1);
|
||||
updateTimeSpan.innerText = `${duration} ms`;
|
||||
if (tree) tree.delete();
|
||||
tree = newTree;
|
||||
parseCount++;
|
||||
renderTreeOnCodeChange();
|
||||
}
|
||||
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
async function renderTree() {
|
||||
isRendering++;
|
||||
const cursor = tree.walk();
|
||||
|
||||
let currentRenderCount = parseCount;
|
||||
let row = '';
|
||||
let rows = [];
|
||||
let finishedRow = false;
|
||||
let visitedChildren = false;
|
||||
let indentLevel = 0;
|
||||
|
||||
for (let i = 0;; i++) {
|
||||
if (i > 0 && i % 10000 === 0) {
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
if (parseCount !== currentRenderCount) {
|
||||
cursor.delete();
|
||||
isRendering--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let displayName;
|
||||
if (cursor.nodeIsMissing) {
|
||||
displayName = `MISSING ${cursor.nodeType}`
|
||||
} else if (cursor.nodeIsNamed) {
|
||||
displayName = cursor.nodeType;
|
||||
}
|
||||
|
||||
if (visitedChildren) {
|
||||
if (displayName) {
|
||||
finishedRow = true;
|
||||
}
|
||||
|
||||
if (cursor.gotoNextSibling()) {
|
||||
visitedChildren = false;
|
||||
} else if (cursor.gotoParent()) {
|
||||
visitedChildren = true;
|
||||
indentLevel--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (displayName) {
|
||||
if (finishedRow) {
|
||||
row += '</div>';
|
||||
rows.push(row);
|
||||
finishedRow = false;
|
||||
}
|
||||
const start = cursor.startPosition;
|
||||
const end = cursor.endPosition;
|
||||
const id = cursor.nodeId;
|
||||
row = `<div>${' '.repeat(indentLevel)}<a class='plain' href="#" data-id=${id} data-range="${start.row},${start.column},${end.row},${end.column}">${displayName}</a> [${start.row}, ${start.column}] - [${end.row}, ${end.column}])`;
|
||||
finishedRow = true;
|
||||
}
|
||||
|
||||
if (cursor.gotoFirstChild()) {
|
||||
visitedChildren = false;
|
||||
indentLevel++;
|
||||
} else {
|
||||
visitedChildren = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finishedRow) {
|
||||
row += '</div>';
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
cursor.delete();
|
||||
cluster.update(rows);
|
||||
treeRows = rows;
|
||||
isRendering--;
|
||||
handleCursorMovement();
|
||||
}
|
||||
|
||||
function handleCursorMovement() {
|
||||
if (isRendering) return;
|
||||
|
||||
const selection = codeEditor.getDoc().listSelections()[0];
|
||||
let start = {row: selection.anchor.line, column: selection.anchor.ch};
|
||||
let end = {row: selection.head.line, column: selection.head.ch};
|
||||
if (
|
||||
start.row > end.row ||
|
||||
(
|
||||
start.row === end.row &&
|
||||
start.column > end.column
|
||||
)
|
||||
) {
|
||||
let swap = end;
|
||||
end = start;
|
||||
start = swap;
|
||||
}
|
||||
const node = tree.rootNode.namedDescendantForPosition(start, end);
|
||||
if (treeRows) {
|
||||
if (treeRowHighlightedIndex !== -1) {
|
||||
treeRows[treeRowHighlightedIndex] = treeRows[treeRowHighlightedIndex].replace('highlighted', 'plain');
|
||||
}
|
||||
treeRowHighlightedIndex = treeRows.findIndex(row => row.includes(`data-id=${node.id}`));
|
||||
if (treeRowHighlightedIndex !== -1) {
|
||||
treeRows[treeRowHighlightedIndex] = treeRows[treeRowHighlightedIndex].replace('plain', 'highlighted');
|
||||
}
|
||||
cluster.update(treeRows);
|
||||
const lineHeight = cluster.options.item_height;
|
||||
const scrollTop = outputContainerScroll.scrollTop;
|
||||
const containerHeight = outputContainerScroll.clientHeight;
|
||||
const offset = treeRowHighlightedIndex * lineHeight;
|
||||
if (scrollTop > offset - 20) {
|
||||
$(outputContainerScroll).animate({scrollTop: offset - 20}, 150);
|
||||
} else if (scrollTop < offset + lineHeight + 40 - containerHeight) {
|
||||
$(outputContainerScroll).animate({scrollTop: offset - containerHeight + 40}, 150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTreeClick(event) {
|
||||
if (event.target.tagName === 'A') {
|
||||
event.preventDefault();
|
||||
const [startRow, startColumn, endRow, endColumn] = event
|
||||
.target
|
||||
.dataset
|
||||
.range
|
||||
.split(',')
|
||||
.map(n => parseInt(n));
|
||||
codeEditor.focus();
|
||||
codeEditor.setSelection(
|
||||
{line: startRow, ch: startColumn},
|
||||
{line: endRow, ch: endColumn}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoggingChange() {
|
||||
if (loggingCheckbox.checked) {
|
||||
parser.setLogger(console.log);
|
||||
} else {
|
||||
parser.setLogger(null);
|
||||
}
|
||||
}
|
||||
|
||||
function treeEditForEditorChange(change) {
|
||||
const oldLineCount = change.removed.length;
|
||||
const newLineCount = change.text.length;
|
||||
const lastLineLength = change.text[newLineCount - 1].length;
|
||||
|
||||
const startPosition = {row: change.from.line, column: change.from.ch};
|
||||
const oldEndPosition = {row: change.to.line, column: change.to.ch};
|
||||
const newEndPosition = {
|
||||
row: startPosition.row + newLineCount - 1,
|
||||
column: newLineCount === 1
|
||||
? startPosition.column + lastLineLength
|
||||
: lastLineLength
|
||||
};
|
||||
|
||||
const startIndex = codeEditor.indexFromPos(change.from);
|
||||
let newEndIndex = startIndex + newLineCount - 1;
|
||||
let oldEndIndex = startIndex + oldLineCount - 1;
|
||||
for (let i = 0; i < newLineCount; i++) newEndIndex += change.text[i].length;
|
||||
for (let i = 0; i < oldLineCount; i++) oldEndIndex += change.removed[i].length;
|
||||
|
||||
return {
|
||||
startIndex, oldEndIndex, newEndIndex,
|
||||
startPosition, oldEndPosition, newEndPosition
|
||||
};
|
||||
}
|
||||
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ permalink: playground
|
|||
---
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.css">
|
||||
|
||||
<h1>Playground</h1>
|
||||
|
||||
|
|
@ -27,23 +28,13 @@ permalink: playground
|
|||
<label for="logging-checkbox">Log</label>
|
||||
|
||||
<textarea id="code-input">
|
||||
function quicksort() {
|
||||
function sort(items) {
|
||||
if (items.length <= 1) return items;
|
||||
var pivot = items.shift(), current, left = [], right = [];
|
||||
while (items.length > 0) {
|
||||
current = items.shift();
|
||||
current < pivot ? left.push(current) : right.push(current);
|
||||
}
|
||||
return sort(left).concat(pivot).concat(sort(right));
|
||||
};
|
||||
return sort(Array.apply(this, arguments));
|
||||
};
|
||||
</textarea>
|
||||
|
||||
<h4>Tree</h4>
|
||||
<span id="update-time"></span>
|
||||
<pre id="output-container" class="highlight"></pre>
|
||||
<div id="output-container-scroll">
|
||||
<pre id="output-container" class="highlight"></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -57,4 +48,5 @@ function quicksort() {
|
|||
<script src="https://tree-sitter.github.io/tree-sitter.js"></script>
|
||||
{% endif %}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.js"></script>
|
||||
<script src="{{ '/assets/js/playground.js' | relative_url }}"></script>
|
||||
|
|
|
|||
|
|
@ -233,6 +233,18 @@ bool ts_tree_cursor_current_node_is_named_wasm(const TSTree *tree) {
|
|||
return ts_node_is_named(node);
|
||||
}
|
||||
|
||||
bool ts_tree_cursor_current_node_is_missing_wasm(const TSTree *tree) {
|
||||
TSTreeCursor cursor = unmarshal_cursor(TRANSFER_BUFFER, tree);
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
return ts_node_is_missing(node);
|
||||
}
|
||||
|
||||
const uint32_t ts_tree_cursor_current_node_id_wasm(const TSTree *tree) {
|
||||
TSTreeCursor cursor = unmarshal_cursor(TRANSFER_BUFFER, tree);
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
return (uint32_t)node.id;
|
||||
}
|
||||
|
||||
void ts_tree_cursor_start_position_wasm(const TSTree *tree) {
|
||||
TSTreeCursor cursor = unmarshal_cursor(TRANSFER_BUFFER, tree);
|
||||
TSNode node = ts_tree_cursor_current_node(&cursor);
|
||||
|
|
|
|||
|
|
@ -461,11 +461,21 @@ class TreeCursor {
|
|||
return C._ts_tree_cursor_current_node_type_id_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get nodeId() {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_id_wasm(this.tree[0]);
|
||||
}
|
||||
|
||||
get nodeIsNamed() {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_is_named_wasm(this.tree[0]) === 1;
|
||||
}
|
||||
|
||||
get nodeIsMissing() {
|
||||
marshalTreeCursor(this);
|
||||
return C._ts_tree_cursor_current_node_is_missing_wasm(this.tree[0]) === 1;
|
||||
}
|
||||
|
||||
get startPosition() {
|
||||
marshalTreeCursor(this);
|
||||
C._ts_tree_cursor_start_position_wasm(this.tree[0]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue