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 = `