diff --git a/.gitignore b/.gitignore index 7bffbb8c..5fc189ca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ fuzz-results test/fixtures/grammars/* !test/fixtures/grammars/.gitkeep +docs/assets/js/tree-sitter.js + /target *.rs.bk *.a diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index d000ad37..25be9c2d 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -130,7 +130,7 @@ } }); - $('h1, h2, h3').filter('[id]').each(function() { + $('h2, h3').filter('[id]').each(function() { $(this).html('' + $(this).text() + ''); }); diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index 269cab34..57fe291d 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -116,3 +116,43 @@ body { } } } + +#playground-container { + > .CodeMirror { + height: auto; + max-height: 350px; + border: 1px solid #aaa; + } + + .CodeMirror-scroll { + height: auto; + max-height: 350px; + } + + h4, select, .field { + display: inline-block; + margin-right: 20px; + } + + #logging-checkbox { + height: 15px; + } + + .CodeMirror div.CodeMirror-cursor { + border-left: 3px solid red; + } +} + +#output-container { + position: relative; + margin-top: 0; + overflow: auto; + padding: 20px 10px; + max-height: 350px; + border: 1px solid #aaa; +} + +.tree-link.highlighted { + background-color: #ddd; + text-decoration: underline; +} diff --git a/docs/assets/js/playground.js b/docs/assets/js/playground.js new file mode 100644 index 00000000..6bb12c26 --- /dev/null +++ b/docs/assets/js/playground.js @@ -0,0 +1,191 @@ +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 function main() { + await TreeSitter.init(); + parser = new TreeSitter(); + codeEditor = CodeMirror.fromTextArea(codeInput, { + lineNumbers: true, + showCursorWhenSelecting: true + }); + + languageName = languageSelect.value; + codeEditor.on('changes', handleCodeChange); + codeEditor.on('cursorActivity', handleCursorMovement); + 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(); +} + +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; + } + + if (!codeEditor.somethingSelected()) 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); + 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.namedChildCount > 0) { + for (let i = 0, n = node.namedChildCount; i < n; i++) { + result += '\n'; + renderNode(node.namedChild(i), indentLevel + 1); + } + } + result += ')'; + } + + outputContainer.innerHTML = result; +}, 200); + +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 new file mode 100644 index 00000000..5a2bcdaf --- /dev/null +++ b/docs/section-5-playground.html @@ -0,0 +1,60 @@ +--- +layout: default +title: Playground +permalink: playground +--- + + + +

Playground

+ + + + + +{% if jekyll.environment == "development" %} + + +{% else %} + + +{% endif %} + + diff --git a/script/serve-docs b/script/serve-docs index b7776fd1..48b9e480 100755 --- a/script/serve-docs +++ b/script/serve-docs @@ -1,4 +1,25 @@ #!/bin/bash +root=$PWD cd docs -bundle exec jekyll serve "$@" + +bundle exec jekyll serve "$@" & + +bundle exec ruby <