From 73e4db41b03e275308aa75314e29fc0d63aa867e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 30 Apr 2019 09:52:33 -0700 Subject: [PATCH] Render syntax tree efficiently in docs playground --- docs/assets/css/style.scss | 9 +- docs/assets/js/playground.js | 420 ++++++++++++++++++++------------- docs/section-5-playground.html | 18 +- lib/web/binding.c | 12 + lib/web/binding.js | 10 + 5 files changed, 284 insertions(+), 185 deletions(-) diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index 57fe291d..7e4b4bb2 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -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; } diff --git a/docs/assets/js/playground.js b/docs/assets/js/playground.js index 0b7e8127..5f8ff923 100644 --- a/docs/assets/js/playground.js +++ b/docs/assets/js/playground.js @@ -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 += "(${type}` - 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 += ''; + rows.push(row); + finishedRow = false; + } + const start = cursor.startPosition; + const end = cursor.endPosition; + const id = cursor.nodeId; + row = `
${' '.repeat(indentLevel)}${displayName} [${start.row}, ${start.column}] - [${end.row}, ${end.column}])`; + finishedRow = true; + } + + if (cursor.gotoFirstChild()) { + visitedChildren = false; + indentLevel++; + } else { + visitedChildren = true; + } + } + } + if (finishedRow) { + row += '
'; + 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); + }; + } +})(); diff --git a/docs/section-5-playground.html b/docs/section-5-playground.html index 5480da6c..ffede392 100644 --- a/docs/section-5-playground.html +++ b/docs/section-5-playground.html @@ -5,6 +5,7 @@ permalink: playground --- +

Playground

@@ -27,23 +28,13 @@ permalink: playground

Tree

-

+
+

+
@@ -57,4 +48,5 @@ function quicksort() { {% endif %} + diff --git a/lib/web/binding.c b/lib/web/binding.c index 29818e5e..99e7d8ce 100644 --- a/lib/web/binding.c +++ b/lib/web/binding.c @@ -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); diff --git a/lib/web/binding.js b/lib/web/binding.js index 318ccf1f..89fcfa91 100644 --- a/lib/web/binding.js +++ b/lib/web/binding.js @@ -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]);