Skip to content

Instantly share code, notes, and snippets.

@cognominal
Created September 5, 2023 00:51
Show Gist options
  • Save cognominal/741536e32e6fb3337751c29dc0c1b5bf to your computer and use it in GitHub Desktop.
Save cognominal/741536e32e6fb3337751c29dc0c1b5bf to your computer and use it in GitHub Desktop.
treesitter codetout
{
"$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