Created
September 5, 2023 00:51
-
-
Save cognominal/741536e32e6fb3337751c29dc0c1b5bf to your computer and use it in GitHub Desktop.
treesitter codetout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"$schema": "https://aka.ms/codetour-schema", | |
"title": "treesitter", | |
"steps": [ | |
{ | |
"title": "Introduction", | |
"description": "I use playground as a way to familiarize myself with playground.js" | |
}, | |
{ | |
"file": "docs/assets/js/playground.js", | |
"description": "I focus on the tree\n", | |
"line": 127, | |
"contents": "let tree;\n\n(async () => {\n const CAPTURE_REGEX = /@\\s*([\\w\\._-]+)/g;\n const COLORS_BY_INDEX = [\n 'blue',\n 'chocolate',\n 'darkblue',\n 'darkcyan',\n 'darkgreen',\n 'darkred',\n 'darkslategray',\n 'dimgray',\n 'green',\n 'indigo',\n 'navy',\n 'red',\n 'sienna',\n ];\n\n const scriptURL = document.currentScript.getAttribute('src');\n\n const codeInput = document.getElementById('code-input');\n const languageSelect = document.getElementById('language-select');\n const loggingCheckbox = document.getElementById('logging-checkbox');\n const outputContainer = document.getElementById('output-container');\n const outputContainerScroll = document.getElementById('output-container-scroll');\n const playgroundContainer = document.getElementById('playground-container');\n const queryCheckbox = document.getElementById('query-checkbox');\n const queryContainer = document.getElementById('query-container');\n const queryInput = document.getElementById('query-input');\n const updateTimeSpan = document.getElementById('update-time');\n const languagesByName = {};\n\n loadState();\n\n await TreeSitter.init();\n\n const parser = new TreeSitter();\n const codeEditor = CodeMirror.fromTextArea(codeInput, {\n lineNumbers: true,\n showCursorWhenSelecting: true\n });\n\n const queryEditor = CodeMirror.fromTextArea(queryInput, {\n lineNumbers: true,\n showCursorWhenSelecting: true\n });\n\n const cluster = new Clusterize({\n rows: [],\n noDataText: null,\n contentElem: outputContainer,\n scrollElem: outputContainerScroll\n });\n const renderTreeOnCodeChange = debounce(renderTree, 50);\n const saveStateOnChange = debounce(saveState, 2000);\n const runTreeQueryOnChange = debounce(runTreeQuery, 50);\n\n let languageName = languageSelect.value;\n let treeRows = null;\n let treeRowHighlightedIndex = -1;\n let parseCount = 0;\n let isRendering = 0;\n let query;\n\n codeEditor.on('changes', handleCodeChange);\n codeEditor.on('viewportChange', runTreeQueryOnChange);\n codeEditor.on('cursorActivity', debounce(handleCursorMovement, 150));\n queryEditor.on('changes', debounce(handleQueryChange, 150));\n\n loggingCheckbox.addEventListener('change', handleLoggingChange);\n queryCheckbox.addEventListener('change', handleQueryEnableChange);\n languageSelect.addEventListener('change', handleLanguageChange);\n outputContainer.addEventListener('click', handleTreeClick);\n\n handleQueryEnableChange();\n await handleLanguageChange()\n\n playgroundContainer.style.visibility = 'visible';\n\n async function handleLanguageChange() {\n const newLanguageName = languageSelect.value;\n if (!languagesByName[newLanguageName]) {\n const url = `${LANGUAGE_BASE_URL}/tree-sitter-${newLanguageName}.wasm`\n languageSelect.disabled = true;\n try {\n languagesByName[newLanguageName] = await TreeSitter.Language.load(url);\n } catch (e) {\n console.error(e);\n languageSelect.value = languageName;\n return\n } finally {\n languageSelect.disabled = false;\n }\n }\n\n tree = null;\n languageName = newLanguageName;\n parser.setLanguage(languagesByName[newLanguageName]);\n handleCodeChange();\n handleQueryChange();\n }\n\n async function handleCodeChange(editor, changes) {\n const newText = codeEditor.getValue() + '\\n';\n const edits = tree && changes && changes.map(treeEditForEditorChange);\n\n const start = performance.now();\n if (edits) {\n for (const edit of edits) {\n tree.edit(edit);\n }\n }\n const newTree = parser.parse(newText, tree);\n const duration = (performance.now() - start).toFixed(1);\n\n updateTimeSpan.innerText = `${duration} ms`;\n if (tree) tree.delete();\n tree = newTree;\n parseCount++;\n renderTreeOnCodeChange();\n runTreeQueryOnChange();\n saveStateOnChange();\n }\n\n async function renderTree() {\n isRendering++;\n const cursor = tree.walk();\n\n let currentRenderCount = parseCount;\n let row = '';\n let rows = [];\n let finishedRow = false;\n let visitedChildren = false;\n let indentLevel = 0;\n\n for (let i = 0;; i++) {\n if (i > 0 && i % 10000 === 0) {\n await new Promise(r => setTimeout(r, 0));\n if (parseCount !== currentRenderCount) {\n cursor.delete();\n isRendering--;\n return;\n }\n }\n\n let displayName;\n if (cursor.nodeIsMissing) {\n displayName = `MISSING ${cursor.nodeType}`\n } else if (cursor.nodeIsNamed) {\n displayName = cursor.nodeType;\n }\n\n if (visitedChildren) {\n if (displayName) {\n finishedRow = true;\n }\n\n if (cursor.gotoNextSibling()) {\n visitedChildren = false;\n } else if (cursor.gotoParent()) {\n visitedChildren = true;\n indentLevel--;\n } else {\n break;\n }\n } else {\n if (displayName) {\n if (finishedRow) {\n row += '</div>';\n rows.push(row);\n finishedRow = false;\n }\n const start = cursor.startPosition;\n const end = cursor.endPosition;\n const id = cursor.nodeId;\n let fieldName = cursor.currentFieldName();\n if (fieldName) {\n fieldName += ': ';\n } else {\n fieldName = '';\n }\n row = `<div>${' '.repeat(indentLevel)}${fieldName}<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}]`;\n finishedRow = true;\n }\n\n if (cursor.gotoFirstChild()) {\n visitedChildren = false;\n indentLevel++;\n } else {\n visitedChildren = true;\n }\n }\n }\n if (finishedRow) {\n row += '</div>';\n rows.push(row);\n }\n\n cursor.delete();\n cluster.update(rows);\n treeRows = rows;\n isRendering--;\n handleCursorMovement();\n }\n\n function runTreeQuery(_, startRow, endRow) {\n if (endRow == null) {\n const viewport = codeEditor.getViewport();\n startRow = viewport.from;\n endRow = viewport.to;\n }\n\n codeEditor.operation(() => {\n const marks = codeEditor.getAllMarks();\n marks.forEach(m => m.clear());\n\n if (tree && query) {\n const captures = query.captures(\n tree.rootNode,\n {row: startRow, column: 0},\n {row: endRow, column: 0},\n );\n let lastNodeId;\n for (const {name, node} of captures) {\n if (node.id === lastNodeId) continue;\n lastNodeId = node.id;\n const {startPosition, endPosition} = node;\n codeEditor.markText(\n {line: startPosition.row, ch: startPosition.column},\n {line: endPosition.row, ch: endPosition.column},\n {\n inclusiveLeft: true,\n inclusiveRight: true,\n css: `color: ${colorForCaptureName(name)}`\n }\n );\n }\n }\n });\n }\n\n function handleQueryChange() {\n if (query) {\n query.delete();\n query.deleted = true;\n query = null;\n }\n\n queryEditor.operation(() => {\n queryEditor.getAllMarks().forEach(m => m.clear());\n if (!queryCheckbox.checked) return;\n\n const queryText = queryEditor.getValue();\n\n try {\n query = parser.getLanguage().query(queryText);\n let match;\n\n let row = 0;\n queryEditor.eachLine((line) => {\n while (match = CAPTURE_REGEX.exec(line.text)) {\n queryEditor.markText(\n {line: row, ch: match.index},\n {line: row, ch: match.index + match[0].length},\n {\n inclusiveLeft: true,\n inclusiveRight: true,\n css: `color: ${colorForCaptureName(match[1])}`\n }\n );\n }\n row++;\n });\n } catch (error) {\n const startPosition = queryEditor.posFromIndex(error.index);\n const endPosition = {\n line: startPosition.line,\n ch: startPosition.ch + (error.length || Infinity)\n };\n\n if (error.index === queryText.length) {\n if (startPosition.ch > 0) {\n startPosition.ch--;\n } else if (startPosition.row > 0) {\n startPosition.row--;\n startPosition.column = Infinity;\n }\n }\n\n queryEditor.markText(\n startPosition,\n endPosition,\n {\n className: 'query-error',\n inclusiveLeft: true,\n inclusiveRight: true,\n attributes: {title: error.message}\n }\n );\n }\n });\n\n runTreeQuery();\n saveQueryState();\n }\n\n function handleCursorMovement() {\n if (isRendering) return;\n\n const selection = codeEditor.getDoc().listSelections()[0];\n let start = {row: selection.anchor.line, column: selection.anchor.ch};\n let end = {row: selection.head.line, column: selection.head.ch};\n if (\n start.row > end.row ||\n (\n start.row === end.row &&\n start.column > end.column\n )\n ) {\n let swap = end;\n end = start;\n start = swap;\n }\n const node = tree.rootNode.namedDescendantForPosition(start, end);\n if (treeRows) {\n if (treeRowHighlightedIndex !== -1) {\n const row = treeRows[treeRowHighlightedIndex];\n if (row) treeRows[treeRowHighlightedIndex] = row.replace('highlighted', 'plain');\n }\n treeRowHighlightedIndex = treeRows.findIndex(row => row.includes(`data-id=${node.id}`));\n if (treeRowHighlightedIndex !== -1) {\n const row = treeRows[treeRowHighlightedIndex];\n if (row) treeRows[treeRowHighlightedIndex] = row.replace('plain', 'highlighted');\n }\n cluster.update(treeRows);\n const lineHeight = cluster.options.item_height;\n const scrollTop = outputContainerScroll.scrollTop;\n const containerHeight = outputContainerScroll.clientHeight;\n const offset = treeRowHighlightedIndex * lineHeight;\n if (scrollTop > offset - 20) {\n $(outputContainerScroll).animate({scrollTop: offset - 20}, 150);\n } else if (scrollTop < offset + lineHeight + 40 - containerHeight) {\n $(outputContainerScroll).animate({scrollTop: offset - containerHeight + 40}, 150);\n }\n }\n }\n\n function handleTreeClick(event) {\n if (event.target.tagName === 'A') {\n event.preventDefault();\n const [startRow, startColumn, endRow, endColumn] = event\n .target\n .dataset\n .range\n .split(',')\n .map(n => parseInt(n));\n codeEditor.focus();\n codeEditor.setSelection(\n {line: startRow, ch: startColumn},\n {line: endRow, ch: endColumn}\n );\n }\n }\n\n function handleLoggingChange() {\n if (loggingCheckbox.checked) {\n parser.setLogger((message, lexing) => {\n if (lexing) {\n console.log(\" \", message)\n } else {\n console.log(message)\n }\n });\n } else {\n parser.setLogger(null);\n }\n }\n\n function handleQueryEnableChange() {\n if (queryCheckbox.checked) {\n queryContainer.style.visibility = '';\n queryContainer.style.position = '';\n } else {\n queryContainer.style.visibility = 'hidden';\n queryContainer.style.position = 'absolute';\n }\n handleQueryChange();\n }\n\n function treeEditForEditorChange(change) {\n const oldLineCount = change.removed.length;\n const newLineCount = change.text.length;\n const lastLineLength = change.text[newLineCount - 1].length;\n\n const startPosition = {row: change.from.line, column: change.from.ch};\n const oldEndPosition = {row: change.to.line, column: change.to.ch};\n const newEndPosition = {\n row: startPosition.row + newLineCount - 1,\n column: newLineCount === 1\n ? startPosition.column + lastLineLength\n : lastLineLength\n };\n\n const startIndex = codeEditor.indexFromPos(change.from);\n let newEndIndex = startIndex + newLineCount - 1;\n let oldEndIndex = startIndex + oldLineCount - 1;\n for (let i = 0; i < newLineCount; i++) newEndIndex += change.text[i].length;\n for (let i = 0; i < oldLineCount; i++) oldEndIndex += change.removed[i].length;\n\n return {\n startIndex, oldEndIndex, newEndIndex,\n startPosition, oldEndPosition, newEndPosition\n };\n }\n\n function colorForCaptureName(capture) {\n const id = query.captureNames.indexOf(capture);\n return COLORS_BY_INDEX[id % COLORS_BY_INDEX.length];\n }\n\n function loadState() {\n const language = localStorage.getItem(\"language\");\n const sourceCode = localStorage.getItem(\"sourceCode\");\n const query = localStorage.getItem(\"query\");\n const queryEnabled = localStorage.getItem(\"queryEnabled\");\n if (language != null && sourceCode != null && query != null) {\n queryInput.value = query;\n codeInput.value = sourceCode;\n languageSelect.value = language;\n queryCheckbox.checked = (queryEnabled === 'true');\n }\n }\n\n function saveState() {\n localStorage.setItem(\"language\", languageSelect.value);\n localStorage.setItem(\"sourceCode\", codeEditor.getValue());\n saveQueryState();\n }\n\n function saveQueryState() {\n localStorage.setItem(\"queryEnabled\", queryCheckbox.checked);\n localStorage.setItem(\"query\", queryEditor.getValue());\n }\n\n function debounce(func, wait, immediate) {\n var timeout;\n return function() {\n var context = this, args = arguments;\n var later = function() {\n timeout = null;\n if (!immediate) func.apply(context, args);\n };\n var callNow = immediate && !timeout;\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n if (callNow) func.apply(context, args);\n };\n }\n})();\n" | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment