feat: update the playground from upstream reference

This commit is contained in:
maunzCache
2025-03-30 15:04:50 +02:00
committed by Michael Hoffmann
parent 3a58dc8928
commit f5ebf32b8c
6 changed files with 4318 additions and 143 deletions

View File

@@ -1,83 +1,110 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" /> <head>
<title>Tree Sitter HCL Playground</title> <meta charset="utf-8" />
<style> <title>Tree Sitter HCL Playground</title>
#playground-container { <style>
max-width: 640px; #playground-container {
margin-left: auto; max-width: 640px;
margin-right: auto; margin-left: auto;
} margin-right: auto;
#playground-container .CodeMirror { }
border: 1px solid;
} #playground-container .CodeMirror {
#create-issue-btn { border: 1px solid;
padding: 0.2em; }
float: right;
font-size: 1.5em; #create-issue-btn {
} padding: 0.2em;
#checkboxes { float: right;
padding-bottom: 1em; font-size: 1.5em;
} }
#output-container {
border: 1px solid; #checkboxes {
} padding-bottom: 1em;
.highlight { }
background-color: #f8f8f8;
} #output-container {
</style> border: 1px solid;
</head> }
<body>
<!-- .highlight {
background-color: #f8f8f8;
}
</style>
</head>
<body>
<!--
This file is licensed under MIT license This file is licensed under MIT license
Copyright (c) 2018 Max Brunsfeld Taken from https://github.com/tree-sitter/tree-sitter/blob/master/docs/src/7-playground.md
Taken from https://github.com/tree-sitter/tree-sitter/docs/section-7-playground.html -->
-->
<link <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
rel="stylesheet" <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.css">
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.css"
/>
<div id="playground-container"> <h1>Tree Sitter HCL Playground</h1>
<h1>Tree Sitter HCL Playground</h1>
<h4>Code</h4>
<div id="checkboxes">
<input id="logging-checkbox" type="checkbox" />
<label for="logging-checkbox">Log</label>
<input id="query-checkbox" type="checkbox" /> <div id="playground-container" class="ts-playground" style="visibility: hidden;">
<label for="query-checkbox">Query</label> <h2>Code</h2>
<div class="custom-select">
<button id="language-button" class="select-button">
<span class="selected-value">JavaScript</span>
<svg class="arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="select-dropdown">
<div class="option" data-value="hcl">HCL</div>
</div> </div>
<select id="language-select" style="display: none;">
<option value="hcl">HCL</option>
</select>
</div>
<textarea id="code-input"> <input id="logging-checkbox" type="checkbox"></input>
<label for="logging-checkbox">Log</label>
<input id="anonymous-nodes-checkbox" type="checkbox"></input>
<label for="anonymous-nodes-checkbox">Show anonymous nodes</label>
<input id="query-checkbox" type="checkbox"></input>
<label for="query-checkbox">Query</label>
<input id="accessibility-checkbox" type="checkbox"></input>
<label for="accessibility-checkbox">Accessibility</label>
<textarea id="code-input">
example "test" { example "test" {
foo = "bar" foo = "bar"
} }
</textarea> </textarea>
<div id="query-container" style="visibility: hidden; position: absolute"> <div id="query-container" style="visibility: hidden; position: absolute;">
<h4>Query</h4> <h2>Query</h2>
<textarea id="query-input"></textarea> <textarea id="query-input"></textarea>
</div>
<h4>Tree</h4>
<span id="update-time"></span>
<div id="output-container-scroll">
<pre id="output-container" class="highlight"></pre>
</div>
<button id="create-issue-btn" type="button">Create Issue</button>
</div> </div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script> <h2>Tree</h2>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.js"></script> <span id="update-time"></span>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.js"></script> <div id="output-container-scroll">
<script src="./vendor/tree-sitter.js"></script> <pre id="output-container" class="highlight"></pre>
<script id="playground-script" src="./playground.js?v=3"></script> </div>
</body> <button id="create-issue-btn" type="button">Create Issue</button>
</html> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script id="playground-script" src="./playground.js?v=3"></script>
<script type="module">
import * as TreeSitter from './vendor/web-tree-sitter.js';
window.TreeSitter = TreeSitter;
setTimeout(() => window.initializePlayground({ local: false }), 1);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
</body>
</html>

View File

@@ -1,49 +1,146 @@
// This file is licensed under MIT license // This file is licensed under MIT license
// Copyright (c) 2018 Max Brunsfeld // Taken from https://github.com/tree-sitter/tree-sitter/docs/src/assets/js/playground.js
// Taken from https://github.com/tree-sitter/tree-sitter/docs/assets/playground.js
let tree; function initializeLocalTheme() {
const themeToggle = document.getElementById('theme-toggle');
if (!themeToggle) return;
// Load saved theme or use system preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
// Set initial theme
document.documentElement.setAttribute('data-theme', initialTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
}
function initializeCustomSelect({ initialValue = null, addListeners = false }) {
const button = document.getElementById('language-button');
const select = document.getElementById('language-select');
if (!button || !select) return;
const dropdown = button.nextElementSibling;
const selectedValue = button.querySelector('.selected-value');
if (initialValue) {
select.value = initialValue;
}
if (select.selectedIndex >= 0 && select.options[select.selectedIndex]) {
selectedValue.textContent = select.options[select.selectedIndex].text;
} else {
selectedValue.textContent = 'JavaScript';
}
if (addListeners) {
button.addEventListener('click', (e) => {
e.preventDefault(); // Prevent form submission
dropdown.classList.toggle('show');
});
document.addEventListener('click', (e) => {
if (!button.contains(e.target)) {
dropdown.classList.remove('show');
}
});
dropdown.querySelectorAll('.option').forEach(option => {
option.addEventListener('click', () => {
selectedValue.textContent = option.textContent;
select.value = option.dataset.value;
dropdown.classList.remove('show');
const event = new Event('change');
select.dispatchEvent(event);
});
});
}
}
window.initializePlayground = async (opts) => {
const { Parser, Language } = window.TreeSitter;
const { local } = opts;
if (local) {
initializeLocalTheme();
}
initializeCustomSelect({ addListeners: true });
let tree;
(async () => {
const CAPTURE_REGEX = /@\s*([\w\._-]+)/g; const CAPTURE_REGEX = /@\s*([\w\._-]+)/g;
const COLORS_BY_INDEX = [ const LIGHT_COLORS = [
"blue", "#0550ae", // blue
"chocolate", "#ab5000", // rust brown
"darkblue", "#116329", // forest green
"darkcyan", "#844708", // warm brown
"darkgreen", "#6639ba", // purple
"darkred", "#7d4e00", // orange brown
"darkslategray", "#0969da", // bright blue
"dimgray", "#1a7f37", // green
"green", "#cf222e", // red
"indigo", "#8250df", // violet
"navy", "#6e7781", // gray
"red", "#953800", // dark orange
"sienna", "#1b7c83" // teal
]; ];
const scriptURL = document.getElementById("playground-script").src; const DARK_COLORS = [
"#79c0ff", // light blue
"#ffa657", // orange
"#7ee787", // light green
"#ff7b72", // salmon
"#d2a8ff", // light purple
"#ffa198", // pink
"#a5d6ff", // pale blue
"#56d364", // bright green
"#ff9492", // light red
"#e0b8ff", // pale purple
"#9ca3af", // gray
"#ffb757", // yellow orange
"#80cbc4" // light teal
];
const codeInput = document.getElementById("code-input"); const codeInput = document.getElementById("code-input");
const languageSelect = document.getElementById("language-select");
const loggingCheckbox = document.getElementById("logging-checkbox"); const loggingCheckbox = document.getElementById("logging-checkbox");
const anonymousNodes = document.getElementById('anonymous-nodes-checkbox');
const outputContainer = document.getElementById("output-container"); const outputContainer = document.getElementById("output-container");
const createIssueBtn = document.getElementById("create-issue-btn");
const outputContainerScroll = document.getElementById( const outputContainerScroll = document.getElementById(
"output-container-scroll", "output-container-scroll",
); );
const playgroundContainer = document.getElementById("playground-container"); const playgroundContainer = document.getElementById("playground-container");
const queryCheckbox = document.getElementById("query-checkbox"); const queryCheckbox = document.getElementById("query-checkbox");
const createIssueBtn = document.getElementById("create-issue-btn");
const queryContainer = document.getElementById("query-container"); const queryContainer = document.getElementById("query-container");
const queryInput = document.getElementById("query-input"); const queryInput = document.getElementById("query-input");
const accessibilityCheckbox = document.getElementById("accessibility-checkbox");
const updateTimeSpan = document.getElementById("update-time"); const updateTimeSpan = document.getElementById("update-time");
const languagesByName = {};
loadState(); loadState();
await TreeSitter.init(); await Parser.init();
const parser = new Parser();
console.log(parser, codeInput, queryInput);
const parser = new TreeSitter();
const codeEditor = CodeMirror.fromTextArea(codeInput, { const codeEditor = CodeMirror.fromTextArea(codeInput, {
lineNumbers: true, lineNumbers: true,
showCursorWhenSelecting: true, showCursorWhenSelecting: true
});
codeEditor.on('keydown', (_, event) => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.stopPropagation(); // Prevent mdBook from going back/forward
}
}); });
const queryEditor = CodeMirror.fromTextArea(queryInput, { const queryEditor = CodeMirror.fromTextArea(queryInput, {
@@ -51,6 +148,12 @@ let tree;
showCursorWhenSelecting: true, showCursorWhenSelecting: true,
}); });
queryEditor.on('keydown', (_, event) => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.stopPropagation(); // Prevent mdBook from going back/forward
}
});
const cluster = new Clusterize({ const cluster = new Clusterize({
rows: [], rows: [],
noDataText: null, noDataText: null,
@@ -61,7 +164,7 @@ let tree;
const saveStateOnChange = debounce(saveState, 2000); const saveStateOnChange = debounce(saveState, 2000);
const runTreeQueryOnChange = debounce(runTreeQuery, 50); const runTreeQueryOnChange = debounce(runTreeQuery, 50);
let languageName = "hcl"; let languageName = languageSelect.value;
let treeRows = null; let treeRows = null;
let treeRowHighlightedIndex = -1; let treeRowHighlightedIndex = -1;
let parseCount = 0; let parseCount = 0;
@@ -74,20 +177,37 @@ let tree;
queryEditor.on("changes", debounce(handleQueryChange, 150)); queryEditor.on("changes", debounce(handleQueryChange, 150));
loggingCheckbox.addEventListener("change", handleLoggingChange); loggingCheckbox.addEventListener("change", handleLoggingChange);
anonymousNodes.addEventListener('change', renderTree);
queryCheckbox.addEventListener("change", handleQueryEnableChange); queryCheckbox.addEventListener("change", handleQueryEnableChange);
accessibilityCheckbox.addEventListener("change", handleQueryChange);
languageSelect.addEventListener("change", handleLanguageChange);
outputContainer.addEventListener("click", handleTreeClick); outputContainer.addEventListener("click", handleTreeClick);
createIssueBtn.addEventListener("click", handleCreateIssue); createIssueBtn.addEventListener("click", handleCreateIssue);
handleQueryEnableChange(); handleQueryEnableChange();
await loadLanguage(); await handleLanguageChange();
playgroundContainer.style.visibility = "visible"; playgroundContainer.style.visibility = "visible";
async function loadLanguage() { async function handleLanguageChange() {
const query = new URL(scriptURL).search; const newLanguageName = languageSelect.value;
const url = `tree-sitter-hcl.wasm${query}`; if (!languagesByName[newLanguageName]) {
const language = await TreeSitter.Language.load(url); const url = `tree-sitter-${newLanguageName}.wasm`;
languageSelect.disabled = true;
try {
languagesByName[newLanguageName] = await Language.load(url);
} catch (e) {
console.error(e);
languageSelect.value = languageName;
return;
} finally {
languageSelect.disabled = false;
}
}
tree = null; tree = null;
parser.setLanguage(language); languageName = newLanguageName;
parser.setLanguage(languagesByName[newLanguageName]);
handleCodeChange(); handleCodeChange();
handleQueryChange(); handleQueryChange();
} }
@@ -127,7 +247,7 @@ let tree;
for (let i = 0; ; i++) { for (let i = 0; ; i++) {
if (i > 0 && i % 10000 === 0) { if (i > 0 && i % 10000 === 0) {
await new Promise(r => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
if (parseCount !== currentRenderCount) { if (parseCount !== currentRenderCount) {
cursor.delete(); cursor.delete();
isRendering--; isRendering--;
@@ -137,9 +257,12 @@ let tree;
let displayName; let displayName;
if (cursor.nodeIsMissing) { if (cursor.nodeIsMissing) {
displayName = `MISSING ${cursor.nodeType}`; const nodeTypeText = cursor.nodeIsNamed ? cursor.nodeType : `"${cursor.nodeType}"`;
displayName = `MISSING ${nodeTypeText}`;
} else if (cursor.nodeIsNamed) { } else if (cursor.nodeIsNamed) {
displayName = cursor.nodeType; displayName = cursor.nodeType;
} else if (anonymousNodes.checked) {
displayName = cursor.nodeType
} }
if (visitedChildren) { if (visitedChildren) {
@@ -165,19 +288,25 @@ let tree;
const start = cursor.startPosition; const start = cursor.startPosition;
const end = cursor.endPosition; const end = cursor.endPosition;
const id = cursor.nodeId; const id = cursor.nodeId;
let fieldName = cursor.currentFieldName(); let fieldName = cursor.currentFieldName;
if (fieldName) { if (fieldName) {
fieldName += ": "; fieldName += ": ";
} else { } else {
fieldName = ""; fieldName = "";
} }
row = `<div>${" ".repeat(
indentLevel, const nodeClass =
)}${fieldName}<a class='plain' href="#" data-id=${id} data-range="${ displayName === 'ERROR' || displayName.startsWith('MISSING')
start.row ? 'node-link error'
},${start.column},${end.row},${end.column}">${displayName}</a> [${ : cursor.nodeIsNamed
start.row ? 'node-link named'
}, ${start.column}] - [${end.row}, ${end.column}])`; : 'node-link anonymous';
row = `<div class="tree-row">${" ".repeat(indentLevel)}${fieldName}` +
`<a class='${nodeClass}' href="#" data-id=${id} ` +
`data-range="${start.row},${start.column},${end.row},${end.column}">` +
`${displayName}</a> <span class="position-info">` +
`[${start.row}, ${start.column}] - [${end.row}, ${end.column}]</span>`;
finishedRow = true; finishedRow = true;
} }
@@ -201,6 +330,14 @@ let tree;
handleCursorMovement(); handleCursorMovement();
} }
function getCaptureCSS(name) {
if (accessibilityCheckbox.checked) {
return `color: white; background-color: ${colorForCaptureName(name)}`;
} else {
return `color: ${colorForCaptureName(name)}`;
}
}
function runTreeQuery(_, startRow, endRow) { function runTreeQuery(_, startRow, endRow) {
if (endRow == null) { if (endRow == null) {
const viewport = codeEditor.getViewport(); const viewport = codeEditor.getViewport();
@@ -210,7 +347,7 @@ let tree;
codeEditor.operation(() => { codeEditor.operation(() => {
const marks = codeEditor.getAllMarks(); const marks = codeEditor.getAllMarks();
marks.forEach(m => m.clear()); marks.forEach((m) => m.clear());
if (tree && query) { if (tree && query) {
const captures = query.captures( const captures = query.captures(
@@ -229,7 +366,7 @@ let tree;
{ {
inclusiveLeft: true, inclusiveLeft: true,
inclusiveRight: true, inclusiveRight: true,
css: `color: ${colorForCaptureName(name)}`, css: getCaptureCSS(name),
}, },
); );
} }
@@ -237,6 +374,21 @@ let tree;
}); });
} }
// When we change from a dark theme to a light theme (and vice versa), the colors of the
// captures need to be updated.
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
handleQueryChange();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
function handleQueryChange() { function handleQueryChange() {
if (query) { if (query) {
query.delete(); query.delete();
@@ -245,17 +397,17 @@ let tree;
} }
queryEditor.operation(() => { queryEditor.operation(() => {
queryEditor.getAllMarks().forEach(m => m.clear()); queryEditor.getAllMarks().forEach((m) => m.clear());
if (!queryCheckbox.checked) return; if (!queryCheckbox.checked) return;
const queryText = queryEditor.getValue(); const queryText = queryEditor.getValue();
try { try {
query = parser.getLanguage().query(queryText); query = parser.language.query(queryText);
let match; let match;
let row = 0; let row = 0;
queryEditor.eachLine(line => { queryEditor.eachLine((line) => {
while ((match = CAPTURE_REGEX.exec(line.text))) { while ((match = CAPTURE_REGEX.exec(line.text))) {
queryEditor.markText( queryEditor.markText(
{ line: row, ch: match.index }, { line: row, ch: match.index },
@@ -322,7 +474,7 @@ let tree;
"plain", "plain",
); );
} }
treeRowHighlightedIndex = treeRows.findIndex(row => treeRowHighlightedIndex = treeRows.findIndex((row) =>
row.includes(`data-id=${node.id}`), row.includes(`data-id=${node.id}`),
); );
if (treeRowHighlightedIndex !== -1) { if (treeRowHighlightedIndex !== -1) {
@@ -339,9 +491,9 @@ let tree;
const containerHeight = outputContainerScroll.clientHeight; const containerHeight = outputContainerScroll.clientHeight;
const offset = treeRowHighlightedIndex * lineHeight; const offset = treeRowHighlightedIndex * lineHeight;
if (scrollTop > offset - 20) { if (scrollTop > offset - 20) {
$(outputContainerScroll).animate({ scrollTop: offset - 20 }, 150); outputContainerScroll.animate({ scrollTop: offset - 20 }, 150);
} else if (scrollTop < offset + lineHeight + 40 - containerHeight) { } else if (scrollTop < offset + lineHeight + 40 - containerHeight) {
$(outputContainerScroll).animate( outputContainerScroll.animate(
{ scrollTop: offset - containerHeight + 40 }, { scrollTop: offset - containerHeight + 40 },
150, 150,
); );
@@ -364,7 +516,7 @@ ${outputText}
const queryParams = `title=${encodeURIComponent( const queryParams = `title=${encodeURIComponent(
title, title,
)}&body=${encodeURIComponent(body)}`; )}&body=${encodeURIComponent(body)}`;
const url = `https://github.com/MichaHoffmann/tree-sitter-hcl/issues/new?${queryParams}`; const url = `https://github.com/tree-sitter-grammars/tree-sitter-hcl/issues/new?${queryParams}`;
window.open(url); window.open(url);
} }
@@ -372,7 +524,7 @@ ${outputText}
if (event.target.tagName === "A") { if (event.target.tagName === "A") {
event.preventDefault(); event.preventDefault();
const [startRow, startColumn, endRow, endColumn] = const [startRow, startColumn, endRow, endColumn] =
event.target.dataset.range.split(",").map(n => parseInt(n)); event.target.dataset.range.split(",").map((n) => parseInt(n));
codeEditor.focus(); codeEditor.focus();
codeEditor.setSelection( codeEditor.setSelection(
{ line: startRow, ch: startColumn }, { line: startRow, ch: startColumn },
@@ -440,43 +592,40 @@ ${outputText}
function colorForCaptureName(capture) { function colorForCaptureName(capture) {
const id = query.captureNames.indexOf(capture); const id = query.captureNames.indexOf(capture);
return COLORS_BY_INDEX[id % COLORS_BY_INDEX.length]; const isDark = document.querySelector('html').classList.contains('ayu') ||
} document.querySelector('html').classList.contains('coal') ||
document.querySelector('html').classList.contains('navy');
function storageGetItem(lookupKey) { const colors = isDark ? DARK_COLORS : LIGHT_COLORS;
try { return colors[id % colors.length];
return localStorage.getItem(lookupKey);
} catch {
return null;
}
}
function storageSetItem(lookupKey, value) {
try {
return localStorage.setIem(lookupKey, value);
} catch {}
} }
function loadState() { function loadState() {
const language = storageGetItem("language"); const language = localStorage.getItem("language");
const sourceCode = storageGetItem("sourceCode"); const sourceCode = localStorage.getItem("sourceCode");
const query = storageGetItem("query"); const anonNodes = localStorage.getItem("anonymousNodes");
const queryEnabled = storageGetItem("queryEnabled"); const query = localStorage.getItem("query");
const queryEnabled = localStorage.getItem("queryEnabled");
if (language != null && sourceCode != null && query != null) { if (language != null && sourceCode != null && query != null) {
queryInput.value = query; queryInput.value = query;
codeInput.value = sourceCode; codeInput.value = sourceCode;
languageSelect.value = language;
initializeCustomSelect({ initialValue: language });
anonymousNodes.checked = anonNodes === "true";
queryCheckbox.checked = queryEnabled === "true"; queryCheckbox.checked = queryEnabled === "true";
} }
} }
function saveState() { function saveState() {
storageSetItem("sourceCode", codeEditor.getValue()); localStorage.setItem("language", languageSelect.value);
localStorage.setItem("sourceCode", codeEditor.getValue());
localStorage.setItem("anonymousNodes", anonymousNodes.checked);
saveQueryState(); saveQueryState();
} }
function saveQueryState() { function saveQueryState() {
storageSetItem("queryEnabled", queryCheckbox.checked); localStorage.setItem("queryEnabled", queryCheckbox.checked);
storageSetItem("query", queryEditor.getValue()); localStorage.setItem("query", queryEditor.getValue());
} }
function debounce(func, wait, immediate) { function debounce(func, wait, immediate) {
@@ -494,5 +643,4 @@ ${outputText}
if (callNow) func.apply(context, args); if (callNow) func.apply(context, args);
}; };
} }
})(); };

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

4001
docs/vendor/web-tree-sitter.js vendored Normal file

File diff suppressed because it is too large Load Diff