playground/tree-sitter/concrete-syntax.js (592 lines of code) (raw):
/**
* Concrete Syntax Integration for Tree-sitter Playground
*/
/**
* Conditional logging based on the logging checkbox
*/
function debugLog(...args) {
const loggingCheckbox = document.getElementById('logging-checkbox');
if (loggingCheckbox && loggingCheckbox.checked) {
console.log(...args);
}
}
function debugError(...args) {
const loggingCheckbox = document.getElementById('logging-checkbox');
if (loggingCheckbox && loggingCheckbox.checked) {
console.error(...args);
}
}
/**
* Wrapper class that implements the WasmSyntaxNode interface expected by Rust
*/
class TreeSitterNodeWrapper {
constructor(node) {
this.node = node;
}
get startByte() {
return this.node.startIndex;
}
get endByte() {
return this.node.endIndex;
}
get startRow() {
return this.node.startPosition.row;
}
get startColumn() {
return this.node.startPosition.column;
}
get endRow() {
return this.node.endPosition.row;
}
get endColumn() {
return this.node.endPosition.column;
}
get kind() {
return this.node.type;
}
get childCount() {
return this.node.childCount;
}
child(index) {
const child = this.node.child(index);
return child ? new TreeSitterNodeWrapper(child) : null;
}
children() {
const children = [];
for (let i = 0; i < this.node.childCount; i++) {
const child = this.node.child(i);
if (child) {
children.push(new TreeSitterNodeWrapper(child));
}
}
return children;
}
utf8Text(sourceCode) {
return this.node.text;
}
clone() {
return new TreeSitterNodeWrapper(this.node);
}
walk() {
return new TreeSitterCursorWrapper(this.node);
}
}
/**
* Wrapper class that implements the WasmSyntaxCursor interface expected by Rust
*/
class TreeSitterCursorWrapper {
constructor(node) {
this.currentNode = node;
this.nodeStack = [];
this.indexStack = []; // Track current index at each level
this.currentIndex = 0; // Current index in parent's children
}
node() {
return new TreeSitterNodeWrapper(this.currentNode);
}
gotoFirstChild() {
if (this.currentNode.childCount > 0) {
this.nodeStack.push(this.currentNode);
this.indexStack.push(this.currentIndex);
this.currentNode = this.currentNode.child(0);
this.currentIndex = 0;
return true;
}
return false;
}
gotoNextSibling() {
if (this.nodeStack.length === 0) return false;
const parent = this.nodeStack[this.nodeStack.length - 1];
if (this.currentIndex + 1 < parent.childCount) {
this.currentIndex += 1;
this.currentNode = parent.child(this.currentIndex);
return true;
}
return false;
}
gotoParent() {
if (this.nodeStack.length > 0) {
this.currentNode = this.nodeStack.pop();
this.currentIndex = this.indexStack.pop();
return true;
}
return false;
}
clone() {
const cloned = new TreeSitterCursorWrapper(this.currentNode);
cloned.nodeStack = [...this.nodeStack];
cloned.indexStack = [...this.indexStack];
cloned.currentIndex = this.currentIndex;
return cloned;
}
}
// Helper function to get the concrete syntax editor
function getConcreteSyntaxEditor() {
const editorElement = document.getElementById('concrete-syntax-editor');
if (editorElement && editorElement.CodeMirror) {
return editorElement.CodeMirror;
}
return null;
}
// Initialize UI functionality
function initConcreteSyntaxUI() {
const queryCheckbox = document.getElementById('query-checkbox');
const queryTypeSelector = document.querySelector('.query-type-selector');
const queryTypeRadios = document.querySelectorAll('input[name="query-type"]');
const queryContainer = document.getElementById('query-container');
const concreteSyntaxContainer = document.getElementById('concrete-syntax-container');
const statusText = document.getElementById('concrete-syntax-status');
const languageSelect = document.getElementById('language-select');
// Initialize CodeMirror for concrete syntax
const concreteSyntaxEditor = window.CodeMirror(document.getElementById('concrete-syntax-editor'), {
lineNumbers: true,
lineWrapping: true,
theme: 'default',
placeholder: 'Enter concrete syntax pattern...',
mode: 'text',
tabSize: 2,
indentWithTabs: false,
extraKeys: {
'Ctrl-Enter': function(cm) {
const pattern = cm.getValue().trim();
if (pattern) {
executeConcreteSyntaxPattern(pattern);
}
},
'Cmd-Enter': function(cm) {
const pattern = cm.getValue().trim();
if (pattern) {
executeConcreteSyntaxPattern(pattern);
}
}
}
});
// Show/hide query sections based on query checkbox and type
queryCheckbox.addEventListener('change', function() {
if (this.checked) {
queryTypeSelector.style.display = 'flex';
updateQueryContainerVisibility();
} else {
queryTypeSelector.style.display = 'none';
queryContainer.style.visibility = 'hidden';
queryContainer.style.position = 'absolute';
concreteSyntaxContainer.style.visibility = 'hidden';
concreteSyntaxContainer.style.position = 'absolute';
}
});
// Initialize visibility on page load
if (queryCheckbox.checked) {
queryTypeSelector.style.display = 'flex';
updateQueryContainerVisibility();
}
// Handle query type changes
queryTypeRadios.forEach(radio => {
radio.addEventListener('change', function() {
updateQueryContainerVisibility();
});
});
function updateQueryContainerVisibility() {
const queryType = document.querySelector('input[name="query-type"]:checked').value;
if (queryType === 'tree-sitter') {
queryContainer.style.visibility = 'visible';
queryContainer.style.position = 'relative';
concreteSyntaxContainer.style.visibility = 'hidden';
concreteSyntaxContainer.style.position = 'absolute';
} else {
queryContainer.style.visibility = 'hidden';
queryContainer.style.position = 'absolute';
concreteSyntaxContainer.style.visibility = 'visible';
concreteSyntaxContainer.style.position = 'relative';
}
}
// Auto-execute on input changes
let debounceTimer;
concreteSyntaxEditor.on('change', function(cm) {
const pattern = cm.getValue().trim();
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (pattern) {
executeConcreteSyntaxPattern(pattern);
} else {
clearHighlights();
}
}, 300); // 300ms debounce
});
// Initialize UI
if (queryCheckbox.checked) {
queryTypeSelector.style.display = 'flex';
updateQueryContainerVisibility();
}
}
// Execute concrete syntax pattern with fresh parsing and editor highlighting
function executeConcreteSyntaxPattern(pattern) {
const statusText = document.getElementById('concrete-syntax-status');
statusText.textContent = 'Searching...';
if (!window.ConcreteSyntax) {
statusText.textContent = 'WASM not ready';
return;
}
try {
// Always get fresh source code
let sourceCode = getFreshSourceCode();
if (!sourceCode || !sourceCode.trim()) {
statusText.textContent = 'No code';
clearHighlights();
return;
}
// Always parse fresh tree
const languageSelect = document.getElementById('language-select');
const currentLanguage = languageSelect ? languageSelect.value : 'python';
parseFreshTree(sourceCode, currentLanguage).then(tree => {
if (!tree || !tree.rootNode) {
statusText.textContent = 'Parse failed';
return;
}
// Execute concrete syntax matching
const matches = executeConcreteSyntaxQuery(pattern, tree.rootNode, sourceCode);
// Highlight matches in editor
highlightMatchesInEditor(matches);
statusText.textContent = matches.length > 0 ? `${matches.length} match${matches.length !== 1 ? 'es' : ''}` : 'No matches';
}).catch(err => {
console.error('Parse error:', err);
statusText.textContent = 'Parse error';
});
} catch (error) {
console.error('Error executing concrete syntax pattern:', error);
statusText.textContent = 'Error';
}
}
// Get current source code from playground
function getFreshSourceCode() {
if (window.playground && window.playground.codeEditor) {
return window.playground.codeEditor.getValue();
}
// Fallback to CodeMirror search
const codeMirrorElements = document.querySelectorAll('.CodeMirror');
for (let cm of codeMirrorElements) {
if (cm.CodeMirror && cm.CodeMirror.getValue) {
const code = cm.CodeMirror.getValue();
if (code && code.trim()) {
return code;
}
}
}
// Last fallback to textarea
const codeInput = document.getElementById('code-input');
return codeInput?.value || '';
}
// Parse fresh tree every time
async function parseFreshTree(sourceCode, language) {
if (!window.TreeSitter || !window.TreeSitter.Language) {
throw new Error('TreeSitter not available');
}
const languageWasm = `./assets/tree-sitter-${language}.wasm`;
const lang = await window.TreeSitter.Language.load(languageWasm);
const parser = new window.TreeSitter.Parser();
parser.setLanguage(lang);
return parser.parse(sourceCode);
}
// Direct CodeMirror highlighting
function highlightMatchesInEditor(matches) {
debugLog('highlightMatchesInEditor called with:', matches);
// Clear previous highlights first
clearHighlights();
if (!matches || matches.length === 0) return;
// Find CodeMirror instance directly
const codeMirrorElements = document.querySelectorAll('.CodeMirror');
let codeEditor = null;
for (let cmElement of codeMirrorElements) {
if (cmElement.CodeMirror) {
codeEditor = cmElement.CodeMirror;
break;
}
}
if (!codeEditor) {
debugLog('No CodeMirror instance found');
return;
}
debugLog('Found CodeMirror, highlighting matches');
// Use tree-sitter colors
const colors = [
"#0550ae", "#ab5000", "#116329", "#844708", "#6639ba",
"#7d4e00", "#0969da", "#1a7f37", "#cf222e", "#8250df"
];
matches.forEach((match, index) => {
debugLog('Processing match', index, ':', match);
// Handle both snake_case and camelCase field names
const range = match.range;
const startPoint = range.startPoint || range.start_point;
const endPoint = range.endPoint || range.end_point;
if (range && startPoint && endPoint) {
const from = {
line: startPoint.row,
ch: startPoint.column
};
const to = {
line: endPoint.row,
ch: endPoint.column
};
// Highlight the full match with primary color
codeEditor.markText(from, to, {
css: `background-color: ${colors[0]}20; border: 1px solid ${colors[0]}60;`,
title: `Concrete Syntax Match ${index + 1}`,
className: 'concrete-syntax-match'
});
// Highlight captured variables with different colors
const captures = match.matches;
if (captures && typeof captures === 'object') {
let colorIndex = 1;
debugLog('Match captures:', captures);
// Handle both Map and plain object
const captureEntries = captures instanceof Map ?
Array.from(captures.entries()) :
Object.entries(captures);
captureEntries.forEach(([key, captureData]) => {
debugLog('Capture:', key, captureData);
if (key !== '*' && captureData) {
const captureColor = colors[colorIndex % colors.length];
// Check if captureData has range information (CapturedNode structure)
const captureRange = captureData.range;
const captureStartPoint = captureRange?.startPoint || captureRange?.start_point;
const captureEndPoint = captureRange?.endPoint || captureRange?.end_point;
if (captureRange && captureStartPoint && captureEndPoint) {
// Use exact range from capture data
const captureFrom = {
line: captureStartPoint.row,
ch: captureStartPoint.column
};
const captureTo = {
line: captureEndPoint.row,
ch: captureEndPoint.column
};
debugLog(`Highlighting capture ${key} from (${captureFrom.line},${captureFrom.ch}) to (${captureTo.line},${captureTo.ch})`);
codeEditor.markText(captureFrom, captureTo, {
css: `background-color: ${captureColor}30; border: 1px solid ${captureColor}; border-radius: 2px;`,
title: `${key}: ${captureData.text || captureData}`,
className: 'concrete-syntax-capture'
});
} else if (typeof captureData === 'string') {
// Fallback: find the capture text within the match
const matchText = match.matchedString || match.matched_string || '';
const captureStart = matchText.indexOf(captureData);
if (captureStart >= 0) {
const captureFrom = {
line: from.line,
ch: from.ch + captureStart
};
const captureTo = {
line: from.line,
ch: from.ch + captureStart + captureData.length
};
debugLog(`Highlighting string capture ${key} from (${captureFrom.line},${captureFrom.ch}) to (${captureTo.line},${captureTo.ch})`);
codeEditor.markText(captureFrom, captureTo, {
css: `background-color: ${captureColor}30; border: 1px solid ${captureColor}; border-radius: 2px;`,
title: `${key}: ${captureData}`,
className: 'concrete-syntax-capture'
});
}
}
colorIndex++;
}
});
}
}
});
}
// Clear highlights directly from CodeMirror
function clearHighlights() {
const codeMirrorElements = document.querySelectorAll('.CodeMirror');
for (let cmElement of codeMirrorElements) {
if (cmElement.CodeMirror) {
const cm = cmElement.CodeMirror;
cm.getAllMarks().forEach(mark => {
if (mark.className && (mark.className.includes('concrete-syntax'))) {
mark.clear();
}
});
}
}
}
// Execute concrete syntax matching
window.executeConcreteSyntaxQuery = function(pattern, node, sourceCode) {
if (!window.ConcreteSyntax) {
debugError('Concrete syntax WASM not initialized');
return [];
}
try {
const wrappedNode = new TreeSitterNodeWrapper(node);
const sourceBytes = new TextEncoder().encode(sourceCode);
const matches = window.ConcreteSyntax.getAllMatches(
pattern,
wrappedNode,
sourceBytes,
true, // recursive
null // replace_node
);
debugLog('Concrete syntax matches:', matches);
return matches;
} catch (error) {
debugError('Concrete syntax matching error:', error);
return [];
}
};
// Integrate with playground's highlighting system
function setupPlaygroundIntegration() {
debugLog('Setting up playground integration');
debugLog('window.playground:', window.playground);
// Check if playground and codeEditor are available, retry if not
function attachCodeChangeListener() {
if (window.playground && window.playground.codeEditor) {
debugLog('Attaching code change listener to playground editor');
let debounceTimer;
window.playground.codeEditor.on('change', function() {
debugLog('Code editor change detected');
// Only re-execute if concrete syntax is active and has a pattern
const queryType = document.querySelector('input[name="query-type"]:checked');
if (queryType && queryType.value === 'concrete-syntax') {
debugLog('Concrete syntax is active, checking for pattern');
const concreteSyntaxEditor = getConcreteSyntaxEditor();
if (concreteSyntaxEditor) {
const pattern = concreteSyntaxEditor.getValue().trim();
if (pattern) {
debugLog('Pattern found, executing concrete syntax matching');
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
executeConcreteSyntaxPattern(pattern);
}, 500); // 500ms debounce for code changes
} else {
debugLog('No pattern found');
}
} else {
debugLog('No concrete syntax editor found');
}
} else {
debugLog('Concrete syntax not active');
}
});
return true; // Successfully attached
}
return false; // Failed to attach
}
// Try to attach immediately
if (!attachCodeChangeListener()) {
// If it fails, retry periodically
const retryInterval = setInterval(() => {
if (attachCodeChangeListener()) {
clearInterval(retryInterval);
debugLog('Code change listener attached after retry');
}
}, 100);
// Give up after 10 seconds
setTimeout(() => {
clearInterval(retryInterval);
debugLog('Gave up trying to attach code change listener');
}, 10000);
}
// Get access to playground's CodeMirror instance and highlighting functions
window.playground.highlightConcreteSyntaxMatches = function(matches) {
debugLog('highlightConcreteSyntaxMatches function called with:', matches);
// Clear existing highlights first
const codeEditor = window.playground.codeEditor;
if (!codeEditor) return;
// Clear previous concrete syntax highlights
codeEditor.getAllMarks().forEach(mark => {
if (mark.className && mark.className.startsWith('concrete-syntax')) {
mark.clear();
}
});
if (!matches || matches.length === 0) return;
// Use playground's color scheme for consistency
const colors = window.playground.LIGHT_COLORS || [
"#0550ae", "#ab5000", "#116329", "#844708", "#6639ba",
"#7d4e00", "#0969da", "#1a7f37", "#cf222e", "#8250df"
];
debugLog('Highlighting matches:', matches);
matches.forEach((match, index) => {
debugLog('Match', index, ':', match);
// Handle both snake_case and camelCase field names
const range = match.range;
const startPoint = range.startPoint || range.start_point;
const endPoint = range.endPoint || range.end_point;
if (range && startPoint && endPoint) {
const from = {
line: startPoint.row,
ch: startPoint.column
};
const to = {
line: endPoint.row,
ch: endPoint.column
};
// Highlight the full match with primary color
codeEditor.markText(from, to, {
css: `background-color: ${colors[0]}20; border: 1px solid ${colors[0]}60;`,
title: `Concrete Syntax Match ${index + 1}`
});
// Highlight captured variables with different colors
const captures = match.matches;
if (captures && typeof captures === 'object') {
let colorIndex = 1;
debugLog('Match captures:', captures);
// Handle both Map and plain object
const captureEntries = captures instanceof Map ?
Array.from(captures.entries()) :
Object.entries(captures);
captureEntries.forEach(([key, captureData]) => {
debugLog('Capture:', key, captureData);
if (key !== '*' && captureData) {
const captureColor = colors[colorIndex % colors.length];
// Check if captureData has range information (CapturedNode structure)
const captureRange = captureData.range;
const captureStartPoint = captureRange?.startPoint || captureRange?.start_point;
const captureEndPoint = captureRange?.endPoint || captureRange?.end_point;
if (captureRange && captureStartPoint && captureEndPoint) {
// Use exact range from capture data
const captureFrom = {
line: captureStartPoint.row,
ch: captureStartPoint.column
};
const captureTo = {
line: captureEndPoint.row,
ch: captureEndPoint.column
};
debugLog(`Highlighting capture ${key} from (${captureFrom.line},${captureFrom.ch}) to (${captureTo.line},${captureTo.ch})`);
codeEditor.markText(captureFrom, captureTo, {
css: `background-color: ${captureColor}30; border: 1px solid ${captureColor}; border-radius: 2px;`,
title: `${key}: ${captureData.text || captureData}`
});
} else if (typeof captureData === 'string') {
// Fallback: find the capture text within the match
const matchText = match.matchedString || match.matched_string || '';
const captureStart = matchText.indexOf(captureData);
if (captureStart >= 0) {
const captureFrom = {
line: from.line,
ch: from.ch + captureStart
};
const captureTo = {
line: from.line,
ch: from.ch + captureStart + captureData.length
};
debugLog(`Highlighting string capture ${key} from (${captureFrom.line},${captureFrom.ch}) to (${captureTo.line},${captureTo.ch})`);
codeEditor.markText(captureFrom, captureTo, {
css: `background-color: ${captureColor}30; border: 1px solid ${captureColor}; border-radius: 2px;`,
title: `${key}: ${captureData}`
});
}
}
colorIndex++;
}
});
}
}
});
};
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initConcreteSyntaxUI);
// Debug function to test the integration
window.testConcreteSyntaxIntegration = function() {
const results = {
playground: !!window.playground,
codeEditor: !!(window.playground && window.playground.codeEditor),
concreteSyntax: !!window.ConcreteSyntax,
queryType: null,
pattern: null,
concreteSyntaxEditor: null
};
const queryType = document.querySelector('input[name="query-type"]:checked');
if (queryType) {
results.queryType = queryType.value;
}
const concreteSyntaxEditor = getConcreteSyntaxEditor();
if (concreteSyntaxEditor) {
results.concreteSyntaxEditor = true;
results.pattern = concreteSyntaxEditor.getValue().trim();
} else {
results.concreteSyntaxEditor = false;
}
console.log('Concrete Syntax Integration Status:', results);
if (results.playground && results.codeEditor && results.concreteSyntax) {
console.log('✅ All systems operational!');
if (results.queryType === 'concrete-syntax' && results.pattern) {
console.log('✅ Ready for concrete syntax matching');
} else {
console.log('ℹ️ Switch to concrete syntax mode and enter a pattern to test');
}
} else {
console.log('❌ Some components not ready:', {
needsPlayground: !results.playground,
needsCodeEditor: !results.codeEditor,
needsConcreteSyntax: !results.concreteSyntax
});
}
return results;
};
// Hook into playground initialization to access tree and source
const originalInitializePlayground = window.initializePlayground;
window.initializePlayground = function(options) {
const result = originalInitializePlayground ? originalInitializePlayground(options) : null;
// Set up multiple checks to ensure playground is ready
let setupAttempts = 0;
const maxAttempts = 50; // 5 seconds total
const checkPlayground = setInterval(() => {
setupAttempts++;
if (window.playground && window.playground.codeEditor) {
clearInterval(checkPlayground);
debugLog('Playground fully accessible for concrete syntax integration');
// Give it a bit more time to ensure everything is initialized
setTimeout(() => {
setupPlaygroundIntegration();
}, 100);
} else if (setupAttempts >= maxAttempts) {
clearInterval(checkPlayground);
debugLog('Timeout waiting for playground initialization');
// Try setup anyway in case some parts work
setupPlaygroundIntegration();
}
}, 100);
return result;
};
// Also set up a fallback initialization
document.addEventListener('DOMContentLoaded', function() {
// Wait a bit for everything to load, then try setup
setTimeout(() => {
if (window.playground && window.playground.codeEditor) {
debugLog('Fallback playground integration setup');
setupPlaygroundIntegration();
}
}, 2000);
});