let tree; (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 = {}; await 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; codeEditor.on('changes', handleCodeChange); codeEditor.on('cursorActivity', debounce(handleCursorMovement, 150)); loggingCheckbox.addEventListener('change', handleLoggingChange); languageSelect.addEventListener('change', handleLanguageChange); outputContainer.addEventListener('click', handleTreeClick); 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(); } async function handleCodeChange(editor, changes) { const newText = codeEditor.getValue() + '\n'; const start = performance.now(); if (tree && changes) { for (const change of changes) { tree.edit(treeEditForEditorChange(change)); } } const newTree = parser.parse(newText, tree); const duration = (performance.now() - start).toFixed(1); updateTimeSpan.innerText = `${duration} ms`; if (tree) tree.delete(); tree = newTree; parseCount++; renderTreeOnCodeChange(); } 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; let fieldName = cursor.currentFieldName(); if (fieldName) { fieldName += ': '; } else { fieldName = ''; } row = `
${' '.repeat(indentLevel)}${fieldName}${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) { const row = treeRows[treeRowHighlightedIndex]; if (row) treeRows[treeRowHighlightedIndex] = row.replace('highlighted', 'plain'); } treeRowHighlightedIndex = treeRows.findIndex(row => row.includes(`data-id=${node.id}`)); if (treeRowHighlightedIndex !== -1) { const row = treeRows[treeRowHighlightedIndex]; if (row) treeRows[treeRowHighlightedIndex] = row.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((message, lexing) => { if (lexing) { console.log(" ", message) } else { console.log(message) } }); } 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); }; } })();