GAL / UI /main.js
Clarkoer's picture
fix bugs and error handling
2f664d2
let fitAddon;
document.addEventListener('DOMContentLoaded', async () => {
// Auto-target Flask backend when running from a different origin (e.g., VS Code Live Server on 5500)
const API_BASE = (location.port && location.port !== '5000') ? 'http://localhost:5000' : '';
// Use same-origin Socket.IO connection for portability (works in Docker and cloud)
const socketBase = (location.port && location.port !== '5000') ? 'http://localhost:5000' : undefined;
const socket = socketBase ? io(socketBase, { reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 1000 }) : io({ reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 1000 });
window._galSocket = socket; // Expose globally for inline scripts
let waitingForInput = false;
let userInput = '';
// Allow inline scripts to reset input state on re-run
window._resetInputState = function() { waitingForInput = false; userInput = ''; };
let inputCallback = null;
let variable = ''; // Store the variable name for which we need input
let termInputRegistered = false;
socket.on('connect', () => {
console.log('Socket.IO connected, sid:', socket.id);
});
socket.on('disconnect', () => {
console.log('Socket.IO disconnected');
});
//
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.43.0/min/vs',
'xterm': 'https://cdn.jsdelivr.net/npm/xterm/lib',
'xterm-addon-fit': 'https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit',
} });
require(['vs/editor/editor.main'], function () {
monaco.languages.register({ id: "gal" });
monaco.languages.setMonarchTokensProvider("gal", {
tokenizer: {
root: [
// GAL basic data types
[/\b(tree|seed|leaf|branch|vine)\b/, "type"],
// GAL advanced data types
[/\b(variety|fertile|soil|bundle)\b/, "advancedType"],
// GAL control flow
[/\b(plant|harvest|grow|prune|graft|water|sow|root|sprout|bud|cultivate|tend|skip|reclaim|pollinate)\b/, "control"],
// GAL boolean values
[/\b(sunshine|frost|tr|fs|empty)\b/, "boolean"],
// GAL I/O functions
[/\b(bloom|wither|spring)\b/, "io"],
// Comments
[/\/\/.*/, "comment"],
[/\/\*/, 'comment', '@comment'],
// Numbers (integers and decimals)
[/\d+\.\d+/, "number"],
[/\d+/, "number"],
// Strings
[/"[^"]*"/, "string"],
// Characters
[/'.'/, "character"],
// Operators
[/[+\-*/%<>=!&|~]+/, "operator"],
// Function calls
[/\b[a-zA-Z_]\w*(?=\()/, "functionIdentifier"],
// Identifiers
[/\b[a-zA-Z_]\w*\b/, "identifier"],
// Brackets and braces
[/[\{\}]/, "braces"],
[/[\[\]]/, "bracket"],
[/[\(\)]/, "parenthesis"],
],
comment: [
[/[^*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/\*/, 'comment'],
],
},
});
monaco.languages.setLanguageConfiguration("gal", {
comments: {
blockComment: ["/*", "*/"],
lineComment: "//"
},
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"'},
{ open: "'", close: "'"},
{ open: '/*', close: '*/'},
],
});
monaco.editor.defineTheme("galTheme", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "type", foreground: "#89CFF0", fontStyle: "bold"},
{ token: "advancedType", foreground: "#D4A5FF", fontStyle: "bold"},
{ token: "control", foreground: "#FFD36E", fontStyle: "bold"},
{ token: "io", foreground: "#FF8A5B"},
{ token: "boolean", foreground: "#B6FF85"},
{ token: "number", foreground: "#FFE7AF"},
{ token: "string", foreground: "#A6E3A1"},
{ token: "character", foreground: "#A6E3A1"},
{ token: "operator", foreground: "#F2FFEF"},
{ token: "identifier", foreground: "#E0FFD9"},
{ token: "functionIdentifier", foreground: "#FF8A5B", fontStyle: "bold"},
{ token: "braces", foreground: "#7CD26F"},
{ token: "bracket", foreground: "#7CD26F"},
{ token: "parenthesis", foreground: "#F2FFEF"},
{ token: "comment", foreground: "#6D8F74", fontStyle: "italic" },
],
colors: {
"editor.foreground": "#F2FFEF",
"editor.background": "#18361D",
"editorCursor.foreground": "#F2FFEF",
"editor.lineHighlightBackground": "#204B27",
"editorLineNumber.foreground": "#A6C3A9",
"editorindentGuide.background": "#2A5A35",
"editorindentGuide.activebackground": "#3A7D4A",
"scrollbarSlider.background": "#0F2014",
"scrollbarSlider.hoverBackground": "#15301E",
"scrollbarSlider.activeBackground": "#224B2B"
}
});
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: `root(){\n\tplant("Hello Garden!");\n\t\n\treclaim;\n}`,
language: 'gal',
theme: 'galTheme',
minimap: { enabled: false },
overviewRulerLanes: 0,
automaticLayout: true,
newLineCharacter: "\n",
suggest: {
filterGraceful: false,
showWords: false,
enabled: false,
},
scrollbar: {
vertical: "auto",
horizontal: "auto",
alwaysConsumeMouseWheel: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
},
// Mobile optimizations
quickSuggestions: false,
parameterHints: { enabled: false },
codeLens: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 3,
renderLineHighlight: 'line',
occurrencesHighlight: false,
selectionHighlight: false,
renderValidationDecorations: 'on',
});
// Add line highlighting on click
let currentLineDecCollection = editor.createDecorationsCollection([]);
editor.onDidChangeCursorPosition((e) => {
const lineNumber = e.position.lineNumber;
currentLineDecCollection.set([
{
range: new monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'current-line-highlight',
glyphMarginClassName: 'current-line-glyph'
}
}
]);
});
// (reverted) no status bar cursor updates
// ── Bracket mismatch highlighting ──
let bracketDecCollection = editor.createDecorationsCollection([]);
const BRACKET_PAIRS = { '(': ')', '[': ']', '{': '}' };
const CLOSE_TO_OPEN = { ')': '(', ']': '[', '}': '{' };
function findUnmatchedBrackets(text) {
const unmatched = [];
const stack = [];
let inString = false, inChar = false, inLineComment = false, inBlockComment = false;
let line = 1, col = 1;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
const next = text[i + 1];
// Track newlines
if (ch === '\n') { line++; col = 1; inLineComment = false; continue; }
// Block comment
if (inBlockComment) {
if (ch === '*' && next === '/') { inBlockComment = false; i++; col += 2; }
else col++;
continue;
}
// Line comment
if (inLineComment) { col++; continue; }
// String literal
if (inString) { if (ch === '"') inString = false; col++; continue; }
// Char literal
if (inChar) { if (ch === "'") inChar = false; col++; continue; }
// Start of comment
if (ch === '/' && next === '/') { inLineComment = true; col++; continue; }
if (ch === '/' && next === '*') { inBlockComment = true; i++; col += 2; continue; }
// Start of string/char
if (ch === '"') { inString = true; col++; continue; }
if (ch === "'") { inChar = true; col++; continue; }
// Bracket matching
if (BRACKET_PAIRS[ch]) {
stack.push({ ch, line, col });
} else if (CLOSE_TO_OPEN[ch]) {
if (stack.length && stack[stack.length - 1].ch === CLOSE_TO_OPEN[ch]) {
stack.pop();
} else {
unmatched.push({ line, col }); // unmatched closer
}
}
col++;
}
// Remaining in stack = unmatched openers
stack.forEach(b => unmatched.push({ line: b.line, col: b.col }));
return unmatched;
}
function updateBracketHighlights() {
const text = editor.getValue();
const bad = findUnmatchedBrackets(text);
const decs = bad.map(b => ({
range: new monaco.Range(b.line, b.col, b.line, b.col + 1),
options: {
inlineClassName: 'bracket-mismatch',
hoverMessage: { value: '**Unmatched bracket**' },
overviewRuler: { color: '#ff3322', position: monaco.editor.OverviewRulerLane.Center }
}
}));
bracketDecCollection.set(decs);
}
// ── Missing semicolon detection ──
let semicolonDecCollection = editor.createDecorationsCollection([]);
function findMissingSemicolons(text) {
const lines = text.split('\n');
const missing = [];
let inBlockComment = false;
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const trimmed = raw.trim();
// Track block comments
if (inBlockComment) {
if (trimmed.includes('*/')) inBlockComment = false;
continue;
}
if (trimmed.startsWith('/*')) {
if (!trimmed.includes('*/')) inBlockComment = true;
continue;
}
// Skip empty lines, line comments, and brace-only lines
if (!trimmed || trimmed.startsWith('//')) continue;
// Strip inline comments to get actual code
const codePart = trimmed.replace(/\/\/.*$/, '').trimEnd();
if (!codePart) continue;
// Skip lines that end with { or } (block openers/closers)
if (/[{}]\s*$/.test(codePart)) continue;
// Skip lines that are only a closing brace or opening brace
if (/^[{}]+$/.test(codePart)) continue;
// Skip variety/soil case labels (with or without inline comment)
if (/^(variety|soil)\b/.test(codePart) && codePart.endsWith(':')) continue;
// Skip control flow lines ending with ) — they need { not ;
if (/^(cultivate|grow|tend|spring|bud|pollinate|root)\b.*\)\s*$/.test(codePart)) continue;
// If line doesn't end with ; it's missing one
if (!codePart.endsWith(';') && !codePart.endsWith(':') && !codePart.endsWith('{') && !codePart.endsWith('}')) {
const lineNum = i + 1;
// Find where code ends (before any inline comment) for ghost placement
const commentIdx = raw.indexOf('//');
const codeEnd = commentIdx >= 0 ? raw.substring(0, commentIdx).trimEnd().length : raw.trimEnd().length;
missing.push({ line: lineNum, col: codeEnd });
}
}
return missing;
}
function updateSemicolonWarnings() {
const model = editor.getModel();
const text = editor.getValue();
const bad = findMissingSemicolons(text);
// Yellow squiggly underlines via model markers
const markers = bad.map(b => ({
severity: monaco.MarkerSeverity.Warning,
startLineNumber: b.line,
startColumn: Math.max(1, b.col - 1),
endLineNumber: b.line,
endColumn: b.col + 1,
message: 'Missing semicolon ;'
}));
monaco.editor.setModelMarkers(model, 'gal-semicolons', markers);
semicolonDecCollection.set([]);
}
editor.onDidChangeModelContent(() => {
updateBracketHighlights();
updateSemicolonWarnings();
if (window.markDirty) window.markDirty();
});
// Initial check
updateBracketHighlights();
updateSemicolonWarnings();
// ── Error line highlighting ──
let errorDecCollection = editor.createDecorationsCollection([]);
// Parse error string for line/col.
// Unified format: "<TYPE> error line X[ col Y][:] ..." for LEXICAL/SYNTAX/SEMANTIC/RUNTIME.
// Legacy fallback: "Ln X ..." for older Semantic/Runtime errors.
function parseErrorLocations(errors) {
const locs = [];
const re1 = /(?:LEXICAL|SYNTAX|SEMANTIC|RUNTIME)\s+error\s+line\s+(\d+)(?:\s+col\s+(\d+))?/i;
const re2 = /^Ln\s+(\d+)\s/i;
(errors || []).forEach(err => {
const s = String(err);
let m = re1.exec(s);
if (m) {
const col = m[2] ? parseInt(m[2], 10) : 1;
locs.push({ line: parseInt(m[1], 10), col, msg: s });
return;
}
m = re2.exec(s);
if (m) { locs.push({ line: parseInt(m[1],10), col: 1, msg: s }); }
});
return locs;
}
window._highlightErrors = function(errors) {
const locs = parseErrorLocations(errors);
if (!locs.length) {
errorDecCollection.set([]);
monaco.editor.setModelMarkers(window.editor.getModel(), 'gal-errors', []);
return;
}
// Decoration overlays (red tinted line)
const decorations = locs.map(loc => ({
range: new monaco.Range(loc.line, 1, loc.line, 1),
options: {
isWholeLine: true,
className: 'error-line-highlight',
glyphMarginClassName: 'error-line-glyph',
overviewRuler: { color: '#ff3322', position: monaco.editor.OverviewRulerLane.Full },
hoverMessage: { value: loc.msg }
}
}));
errorDecCollection.set(decorations);
// Also set model markers (squiggly red underlines)
const markers = locs.map(loc => ({
severity: monaco.MarkerSeverity.Error,
startLineNumber: loc.line,
startColumn: loc.col,
endLineNumber: loc.line,
endColumn: loc.col + 20,
message: loc.msg
}));
monaco.editor.setModelMarkers(window.editor.getModel(), 'gal-errors', markers);
// Auto-scroll to first error
const first = locs[0];
window.editor.revealLineInCenter(first.line);
window.editor.setPosition({ lineNumber: first.line, column: first.col });
};
window._clearErrorHighlights = function() {
errorDecCollection.set([]);
monaco.editor.setModelMarkers(window.editor.getModel(), 'gal-errors', []);
};
});
require(['vs/editor/editor.main', 'xterm/xterm', 'xterm-addon-fit'], function (_, Xterm, FitAddon) {
const term = new Xterm.Terminal({
cursorBlink: true,
cursorStyle: 'bar',
scrollback: 5000,
rows: 20,
theme: {
background: '#102417',
foreground: '#f2ffef',
cursor: '#f2ffef',
FontFace: 'monospace',
fontStyle: 'bold',
},
});
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
window._galTerm = term; // Expose globally for inline scripts
window.fitAddon = fitAddon; // Expose for height resizer
setTimeout(() => term.focus(), 100);
term.write('Terminal Ready\r\n');
fitAddon.fit();
window.addEventListener('resize', () => {
fitAddon.fit();
});
// HUD removed: no coins/energy UI
// Toolbar buttons
const btnRun = document.getElementById('btn-run');
const btnLex = document.getElementById('btn-lex');
// File menu (New/Open/Save/Clear) is handled in index.html inline script
// Terminal header actions
let autoScroll = true;
const autoBtn = document.getElementById('term-autoscroll');
const copyBtn = document.getElementById('term-copy');
const clearBtn = document.getElementById('term-clear');
const toggleMobileLexBtn = document.getElementById('toggle-mobile-lexemes-nav');
if (autoBtn) {
autoBtn.setAttribute('aria-pressed', 'true');
autoBtn.addEventListener('click', () => {
autoScroll = !autoScroll;
autoBtn.setAttribute('aria-pressed', String(autoScroll));
});
}
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
try {
const buffer = term.buffer.active;
let lines = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) lines.push(line.translateToString());
}
// Trim trailing empty lines from the buffer
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
lines.pop();
}
await navigator.clipboard.writeText(lines.join('\n') + '\n');
} catch (e) {
console.warn('Copy failed:', e);
}
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => term.clear());
}
// ── Font size +/- ──
const FONT_MIN = 8, FONT_MAX = 24;
let termFontSize = 14;
const fontDecBtn = document.getElementById('term-font-dec');
const fontIncBtn = document.getElementById('term-font-inc');
function updateFontBtnStates() {
if (fontDecBtn) fontDecBtn.disabled = termFontSize <= FONT_MIN;
if (fontIncBtn) fontIncBtn.disabled = termFontSize >= FONT_MAX;
}
updateFontBtnStates();
if (fontDecBtn) {
fontDecBtn.addEventListener('click', () => {
if (termFontSize <= FONT_MIN) return;
termFontSize--;
term.options.fontSize = termFontSize;
fitAddon.fit();
updateFontBtnStates();
});
}
if (fontIncBtn) {
fontIncBtn.addEventListener('click', () => {
if (termFontSize >= FONT_MAX) return;
termFontSize++;
term.options.fontSize = termFontSize;
fitAddon.fit();
updateFontBtnStates();
});
}
// Terminal download button removed
// Mobile lexeme table toggle (navbar button)
if (toggleMobileLexBtn) {
const mobileTokensSection = document.querySelector('.mobile-tokens');
const backdrop = document.getElementById('mobile-tokens-backdrop');
const toggleIcon = document.getElementById('lexeme-toggle-icon');
const closeBtn = document.getElementById('mobile-tokens-close');
const closeLexemeTable = () => {
if (mobileTokensSection) mobileTokensSection.classList.remove('show');
if (backdrop) backdrop.classList.remove('show');
if (toggleMobileLexBtn) toggleMobileLexBtn.setAttribute('aria-expanded', 'false');
if (toggleIcon) toggleIcon.textContent = '▼';
};
toggleMobileLexBtn.addEventListener('click', () => {
if (!mobileTokensSection) return;
const isShowing = mobileTokensSection.classList.toggle('show');
if (backdrop) backdrop.classList.toggle('show', isShowing);
toggleMobileLexBtn.setAttribute('aria-expanded', String(isShowing));
if (toggleIcon) {
toggleIcon.textContent = isShowing ? '▲' : '▼';
}
});
// Close when clicking close button
if (closeBtn) {
closeBtn.addEventListener('click', closeLexemeTable);
}
// Close when clicking backdrop
if (backdrop) {
backdrop.addEventListener('click', closeLexemeTable);
}
}
// Mobile status popup toggle
(function() {
const fab = document.getElementById('mobile-status-fab');
const popup = document.getElementById('mobile-status-popup');
const backdrop = document.getElementById('mobile-status-backdrop');
const closeBtn = document.getElementById('mobile-status-close');
if (!fab || !popup) return;
const syncChips = () => {
let hasError = false;
popup.querySelectorAll('.mobile-status-chip[data-mirror]').forEach(chip => {
const src = document.getElementById(chip.dataset.mirror);
if (src) {
chip.textContent = src.textContent;
chip.className = 'mobile-status-chip';
if (src.classList.contains('ok')) chip.classList.add('ok');
if (src.classList.contains('err')) { chip.classList.add('err'); hasError = true; }
}
});
// Update FAB indicator color
const fabIcon = fab.querySelector('.mobile-status-fab-icon');
if (fabIcon) fabIcon.style.color = hasError ? '#ff8a5b' : '#7cd26f';
};
const closePopup = () => {
popup.classList.remove('show');
if (backdrop) backdrop.classList.remove('show');
};
fab.addEventListener('click', () => {
syncChips();
const isShowing = popup.classList.toggle('show');
if (backdrop) backdrop.classList.toggle('show', isShowing);
});
if (closeBtn) closeBtn.addEventListener('click', closePopup);
if (backdrop) backdrop.addEventListener('click', closePopup);
})();
window.runLexer = async function (options = {}) {
const silent = options.silent === true;
const sourceCode = editor.getValue();
console.log("Running lexer with source code:", sourceCode);
if (!silent) {
// Separate runs with a blank line
term.write('\r\n');
}
try {
const response = await fetch(`${API_BASE}/api/lex`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_code: sourceCode })
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
console.log("Lexer response:", data);
const tokensTableBody = document.getElementById('tokenBody');
tokensTableBody.innerHTML = '';
const tokensTableBodySide = document.getElementById('tokenBodySide');
if (tokensTableBodySide) tokensTableBodySide.innerHTML = '';
const tokensTableBodyMobile = document.getElementById('tokenBodyMobile');
if (tokensTableBodyMobile) tokensTableBodyMobile.innerHTML = '';
// Map backend token types to display names (Token column)
const displayType = (tok) => {
// For identifiers, show 'id' token type
if (tok.type === 'idf' || tok.type === 'id' || tok.type === 'TT_IDENTIFIER') {
return 'id';
}
// For literals, show the token type name
if (tok.type === 'intlit' || tok.type === 'TT_INTEGERLIT' || tok.type === 'seedlit') return 'intlit';
if (tok.type === 'dbllit' || tok.type === 'TT_DOUBLELIT' || tok.type === 'treelit') return 'dbllit';
if (tok.type === 'stringlit' || tok.type === 'strlit' || tok.type === 'strnglit' || tok.type === 'TT_STRINGLIT') return 'stringlit';
if (tok.type === 'chrlit' || tok.type === 'TT_CHARLIT' || tok.type === 'leaflit') return 'chrlit';
// Special case for branch -> t/f
if (tok.type === 'branch') return 't/f';
// Boolean literals: sunshine and frost show their keyword as token
if (tok.type === 'sunshine') return 'sunshine';
if (tok.type === 'frost') return 'frost';
// Reserved words are their type directly
const kwSet = new Set(['water','plant','seed','leaf','branch','tree','spring','wither','bud','harvest','grow','cultivate','tend','empty','prune','skip','reclaim','root','pollinate','variety','fertile','soil','bundle','vine']);
if (kwSet.has(tok.type)) return tok.type;
// Show symbols in the Token column as the actual symbol
const SYMBOLS = new Set(['+','-','*','/','%','=','==','===','+=','-=','*=','/=','%=','<','>','<=','>=','!=','&&','&','||','|','!','++','--','~','`','(',')','{','}','[',']',',',';',';',':','.']);
if (tok && typeof tok.value === 'string' && SYMBOLS.has(tok.value)) return tok.value;
if (tok && typeof tok.type === 'string' && SYMBOLS.has(tok.type)) return tok.type;
// Fallback for any other token types from old or new lexer
if (tok.type && tok.type.startsWith('TT_')) {
// If value looks like a symbol, prefer it
if (typeof tok.value === 'string' && SYMBOLS.has(tok.value)) return tok.value;
return tok.type.substring(3).toLowerCase();
}
return tok.type || '';
};
// Determine token classification (Type column)
const RESERVED = new Set([
'water','plant','seed','leaf','branch','tree','spring','wither','bud','harvest','grow','cultivate','tend','empty','prune','skip','reclaim','root','pollinate','variety','fertile','soil','bundle','vine'
]);
const SYMBOLS = new Set(['+','-','*','/','%','=','==','+=','-=','*=','/=','%=','<','>','<=','>=','!=','&&','||','!','++','--','~','`','(',')','{','}','[',']',',',';',';',':','.']);
const symbolTypeName = (sym) => {
if (sym === '{') return 'R_Curly';
if (sym === '}') return 'L_Curly';
if (sym === '(') return 'L_Paren';
if (sym === ')') return 'R_Paren';
if (sym === '[') return 'L_Brkt';
if (sym === ']') return 'R_Brkt';
if (sym === ',') return 'Comma';
if (sym === ';') return 'Semi_c';
if (sym === ':') return 'Colon';
if (sym === '.') return 'Dot';
return '';
};
const classifyType = (tok) => {
const t = tok.type || '';
// Keep only these categories in Type column
if (RESERVED.has(t)) return 'RW';
if (t === 'idf' || t === 'id' || t === 'TT_IDENTIFIER') return 'ID';
if (t === 'intlit' || t === 'TT_INTEGERLIT') return 'integer';
if (t === 'dbllit' || t === 'TT_DOUBLELIT' || t === 'treelit') return 'double';
if (t === 'stringlit' || t === 'strlit' || t === 'strnglit' || t === 'TT_STRINGLIT') return 'string';
if (t === 'chrlit' || t === 'TT_CHARLIT') return 'character';
// Boolean literals
if (t === 'sunshine' || t === 'frost') return 'false';
// Operators are labeled 'operator'
const OPS = new Set(['+','-','*','/','%','=','==','+=','-=','*=','/=','%=','<','>','<=','>=','!=','&&','||','!','++','--','~','`']);
const lex = (tok.value == null ? '' : String(tok.value));
if (OPS.has(lex) || OPS.has(t)) return 'operator';
// Symbols like braces/parens get specific names
if (SYMBOLS.has(lex)) return symbolTypeName(lex);
if (SYMBOLS.has(t)) return symbolTypeName(t);
// Everything else (operators etc.) blank
return '';
};
// Filter out tokens we don't want displayed in the lexeme tables
const visibleTokens = (data.tokens || []).filter(t => t && t.type !== 'TT_NL' && t.type !== 'TT_EOF' && t.type !== 'EOF');
// Operator tokens should show description in TYPE column
const operatorTokens = new Set(['+', '-', '*', '/', '%', '**', '~', '++', '--',
'=', '+=', '-=', '*=', '/=', '%=', '==', '!=', '<', '>', '<=', '>=',
'&&', '||', '!', '`']);
visibleTokens.forEach(token => {
const vDisp = token.value == null ? '' : String(token.value); // Lexeme column
const tDisp = displayType(token); // Token column - use displayType instead of raw type
// TYPE column: use description for operators, classifyType for others
const cDisp = operatorTokens.has(token.type) ? (token.description || classifyType(token)) : classifyType(token);
const row = tokensTableBody.insertRow();
row.insertCell(0).textContent = vDisp;
row.insertCell(1).textContent = tDisp;
row.insertCell(2).textContent = cDisp;
if (tokensTableBodySide){
const r = tokensTableBodySide.insertRow();
r.insertCell(0).textContent = vDisp;
r.insertCell(1).textContent = tDisp;
r.insertCell(2).textContent = cDisp;
}
if (tokensTableBodyMobile){
const m = tokensTableBodyMobile.insertRow();
m.insertCell(0).textContent = vDisp;
m.insertCell(1).textContent = tDisp;
m.insertCell(2).textContent = cDisp;
}
});
// No table content mirrored to terminal output
if (data.errors.length > 0) {
if (!silent) {
data.errors.forEach(err => {
term.write(`\x1b[1;31m${err}\x1b[0m\r\n`);
});
}
if (!silent && window._highlightErrors) window._highlightErrors(data.errors);
const sl = document.getElementById('status-lex');
if (sl){ sl.classList.remove('ok'); sl.classList.add('err'); sl.textContent = 'Lexical: Error'; }
} else {
if (!silent && window._clearErrorHighlights) window._clearErrorHighlights();
if (!silent) term.write('Lexical analysis successful!\r\n');
const sl = document.getElementById('status-lex');
if (sl){ sl.classList.remove('err'); sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
return true;
}
} catch (error) {
console.error("Error running lexical analysis:", error);
if (!silent) term.write('Error running lexical analysis.\r\n');
return false;
}
};
window.runSyntax = async function (options = {}) {
const silent = options.silent === true;
const sourceCode = editor.getValue();
console.log("Running syntax analysis with source code:", sourceCode);
if (!silent) {
term.write('\r\n');
}
// Run lexer first to populate the token table
await runLexer({ silent: true });
try {
const response = await fetch(`${API_BASE}/api/parse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_code: sourceCode })
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
console.log("Parser response:", data);
// Update status chips
const sl = document.getElementById('status-lex');
const ss = document.getElementById('status-syn');
// Check if stage is an array or string
const stages = Array.isArray(data.stage) ? data.stage : [data.stage];
const hasLexicalErrors = stages.includes('lexical') || data.lexical_errors;
const hasSyntaxErrors = stages.includes('syntax') || data.syntax_errors;
if (data.errors && data.errors.length > 0) {
if (!silent) {
data.errors.forEach(err => term.write(`\x1b[1;31m${err}\x1b[0m\r\n`));
}
if (window._highlightErrors) window._highlightErrors(data.errors);
} else {
if (window._clearErrorHighlights) window._clearErrorHighlights();
}
if (hasLexicalErrors) {
if (sl) { sl.classList.remove('ok'); sl.classList.add('err'); sl.textContent = 'Lexical: Error'; }
} else {
if (sl) { sl.classList.remove('err'); sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
}
if (hasSyntaxErrors) {
if (ss) { ss.classList.remove('ok'); ss.classList.add('err'); ss.textContent = 'Syntax: Error'; }
} else {
if (ss) { ss.classList.remove('err'); ss.classList.add('ok'); ss.textContent = 'Syntax: OK'; }
}
if (data.success && !hasLexicalErrors && !hasSyntaxErrors) {
if (!silent) term.write('Syntax analysis successful!\r\n');
return true;
}
} catch (error) {
console.error("Error running syntax analysis:", error);
if (!silent) term.write('Error running syntax analysis.\r\n');
return false;
}
};
// Semantic Analysis Function
window.runSemantic = async function (options = {}) {
const silent = options.silent || false;
const sourceCode = editor.getValue();
console.log("Running semantic analysis with source code:", sourceCode);
// Run lexer first to populate the token table
await runLexer({ silent: true });
try {
const response = await fetch(`${API_BASE}/api/semantic`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_code: sourceCode })
});
const data = await response.json();
console.log("Semantic response:", data);
// Clear terminal and display analysis results
term.clear();
const sl = document.getElementById('status-lex');
const ss = document.getElementById('status-syn');
const ssem = document.getElementById('status-sem');
if (data.stage === 'lexical' && data.errors.length > 0) {
// Lexical errors
if (!silent) {
term.write('Lexical Errors:\r\n');
data.errors.forEach(err => term.write(` ${err}\r\n`));
}
if (sl) { sl.classList.remove('ok'); sl.classList.add('err'); sl.textContent = 'Lexical: Error'; }
if (ss) { ss.classList.remove('ok', 'err'); ss.textContent = 'Syntax: —'; }
if (ssem) { ssem.classList.remove('ok', 'err'); ssem.textContent = 'Semantic: —'; }
if (window._highlightErrors) window._highlightErrors(data.errors);
} else if (data.stage === 'syntax' && data.errors.length > 0) {
// Syntax errors
if (!silent) {
term.write('Syntax Errors:\r\n');
data.errors.forEach(err => term.write(` ${err}\r\n`));
}
if (sl) { sl.classList.remove('err'); sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
if (ss) { ss.classList.remove('ok'); ss.classList.add('err'); ss.textContent = 'Syntax: Error'; }
if (ssem) { ssem.classList.remove('ok', 'err'); ssem.textContent = 'Semantic: —'; }
if (window._highlightErrors) window._highlightErrors(data.errors);
} else if (data.stage === 'semantic') {
// Semantic analysis results
if (!silent) {
if (data.errors.length > 0) {
term.write('Semantic analysis error!\r\n');
data.errors.forEach(err => term.write(` ${err}\r\n`));
} else {
term.write('Semantic analysis passed!\r\n');
}
}
if (sl) { sl.classList.remove('err'); sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
if (ss) { ss.classList.remove('err'); ss.classList.add('ok'); ss.textContent = 'Syntax: OK'; }
if (ssem) {
if (data.errors.length > 0) {
ssem.classList.remove('ok'); ssem.classList.add('err'); ssem.textContent = 'Semantic: Error';
if (window._highlightErrors) window._highlightErrors(data.errors);
} else {
ssem.classList.remove('err'); ssem.classList.add('ok'); ssem.textContent = 'Semantic: OK';
if (window._clearErrorHighlights) window._clearErrorHighlights();
}
}
}
} catch (error) {
console.error("Error running semantic analysis:", error);
if (!silent) term.write('Error running semantic analysis.\\r\\n');
}
};
// Syntax analysis removed; only lexical phase retained.
// ─── Run / Execute Program ─────────────────────────────
// Helper: update status chips from a stage/success result
function updateStatusChips(stage, success) {
const sl = document.getElementById('status-lex');
const ss = document.getElementById('status-syn');
const ssem = document.getElementById('status-sem');
const sexe = document.getElementById('status-exe');
if (stage === 'lexical') {
if (sl) { sl.classList.add('err'); sl.textContent = 'Lexical: Error'; }
} else if (stage === 'syntax') {
if (sl) { sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
if (ss) { ss.classList.add('err'); ss.textContent = 'Syntax: Error'; }
} else if (stage === 'semantic') {
if (sl) { sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
if (ss) { ss.classList.add('ok'); ss.textContent = 'Syntax: OK'; }
if (ssem) { ssem.classList.add('err'); ssem.textContent = 'Semantic: Error'; }
} else if (stage === 'execution') {
if (sl) { sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
if (ss) { ss.classList.add('ok'); ss.textContent = 'Syntax: OK'; }
if (ssem) { ssem.classList.add('ok'); ssem.textContent = 'Semantic: OK'; }
if (success) {
if (sexe) { sexe.classList.add('ok'); sexe.textContent = 'Execution: OK'; }
} else {
if (sexe) { sexe.classList.add('err'); sexe.textContent = 'Execution: Error'; }
}
}
}
// Run via REST (non-interactive programs)
async function runViaREST(sourceCode, silent) {
try {
const resp = await fetch(`${API_BASE}/api/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_code: sourceCode })
});
const result = await resp.json();
if (result.output && result.output.length > 0) {
result.output.forEach(line => {
const isError = /error|Error/.test(line);
if (isError) {
term.write(`\x1b[1;31m${line}\x1b[0m\r\n`);
} else {
term.write(line + '\r\n');
}
});
}
updateStatusChips(result.stage, result.success);
if (result.stage === 'execution') {
if (result.success) {
if (!silent) term.write('Code execution successful.\r\n');
} else {
if (!silent) term.write('Code execution failed.\r\n');
}
}
} catch (err) {
if (!silent) term.write('Error: Could not connect to server.\r\n');
console.error('Run error:', err);
}
}
// Run via Socket.IO (interactive programs with water())
function runViaSocket(sourceCode, silent) {
// Reset input state from any previous run
waitingForInput = false;
userInput = '';
// Bump generation and clear collected output
window._runGeneration = (window._runGeneration || 0) + 1;
window._socketOutputLog = [];
// Remove any lingering execution_complete listeners
socket.removeAllListeners('execution_complete');
// Ensure socket is connected
if (!socket.connected) {
socket.connect();
}
// Wait until connected, then emit
function doEmit() {
const myGen = window._runGeneration;
// Remove any stale completion listeners before adding a fresh one
socket.removeAllListeners('execution_complete');
// One-time listener for execution_complete
socket.once('execution_complete', function onComplete(data) {
if (myGen !== window._runGeneration) return; // stale
updateStatusChips(data.stage, data.success);
if (data.stage === 'execution') {
if (data.success) {
if (!silent) {
term.write('\r\n\x1b[1;32mCode execution successful.\x1b[0m\r\n');
}
} else {
if (!silent) {
term.write('\r\n\x1b[1;31mCode execution failed.\x1b[0m\r\n');
}
}
}
// Highlight errors from collected socket output
if (!data.success && window._socketOutputLog && window._socketOutputLog.length > 0) {
if (window._highlightErrors) window._highlightErrors(window._socketOutputLog);
} else {
if (window._clearErrorHighlights) window._clearErrorHighlights();
}
});
socket.emit('run_code', { source_code: sourceCode });
}
if (socket.connected) {
doEmit();
} else {
socket.once('connect', () => {
doEmit();
});
// Timeout fallback
setTimeout(() => {
if (!socket.connected) {
if (!silent) term.write('Error: Could not connect to server for interactive mode.\r\n');
}
}, 5000);
}
}
window.runProgram = async function (options = {}) {
const silent = options.silent || false;
const sourceCode = editor.getValue();
// Clear previous error highlights
if (window._clearErrorHighlights) window._clearErrorHighlights();
// Remove stale socket listeners
socket.removeAllListeners('execution_complete');
// Populate the token table in the background (silent — no terminal writes)
await runLexer({ silent: true });
// Reset status chips
const sl = document.getElementById('status-lex');
const ss = document.getElementById('status-syn');
const ssem = document.getElementById('status-sem');
const sexe = document.getElementById('status-exe');
if (sl) { sl.classList.remove('ok','err'); sl.textContent = 'Lexical: —'; }
if (ss) { ss.classList.remove('ok','err'); ss.textContent = 'Syntax: —'; }
if (ssem) { ssem.classList.remove('ok','err'); ssem.textContent = 'Semantic: —'; }
if (sexe) { sexe.classList.remove('ok','err'); sexe.textContent = 'Execution: —'; }
// Check if program uses water() (needs interactive input via Socket.IO)
const needsInput = /\bwater\s*\(/.test(sourceCode);
if (needsInput) {
runViaSocket(sourceCode, silent);
} else {
await runViaREST(sourceCode, silent);
}
};
// Run generation counter — ignore output from stale/previous runs
window._runGeneration = 0;
window._socketOutputLog = [];
socket.on('output', function (data) {
// Ignore output from a previous run
if (data._gen !== undefined && data._gen !== window._runGeneration) return;
const text = data.output;
window._socketOutputLog.push(text);
const isError = /error|Error/.test(text);
const lines = text.split('\n');
lines.forEach((line, index) => {
if (isError) {
term.write(`\x1b[1;31m${line}\x1b[0m`);
} else {
term.write(line);
}
if (index < lines.length - 1) {
term.write('\r\n');
}
});
if (autoScroll) term.scrollToBottom();
term.focus();
});
// Update status chips as each compiler stage completes
socket.on('stage_complete', function (data) {
const sl = document.getElementById('status-lex');
const ss = document.getElementById('status-syn');
const ssem = document.getElementById('status-sem');
if (data.stage === 'lexical' && data.success) {
if (sl) { sl.classList.remove('err'); sl.classList.add('ok'); sl.textContent = 'Lexical: OK'; }
}
if (data.stage === 'syntax' && data.success) {
if (ss) { ss.classList.remove('err'); ss.classList.add('ok'); ss.textContent = 'Syntax: OK'; }
}
if (data.stage === 'semantic' && data.success) {
if (ssem) { ssem.classList.remove('err'); ssem.classList.add('ok'); ssem.textContent = 'Semantic: OK'; }
}
});
// Register ONE persistent onData listener (only once)
if (!termInputRegistered) {
termInputRegistered = true;
term.onData(function (e) {
if (!waitingForInput) return;
if (e === '\x1b[A' || e === '\x1b[B' || e === '\x1b[C' || e === '\x1b[D') {
return;
}
if (e === '\r') {
term.write('\r\n');
waitingForInput = false;
socket.emit('capture_input', { var_name: variable, input: userInput });
userInput = '';
} else if (e === '\u007f') {
if (userInput.length > 0) {
userInput = userInput.slice(0, -1);
term.write('\b \b');
}
} else {
userInput += e;
term.write(e);
}
});
}
socket.on('input_required', function (data) {
const prompt = data.prompt;
variable = data.variable;
waitingForInput = true;
userInput = '';
});
// Global variable to track selected run mode
let currentRunMode = 'run';
window.runCode = async function () {
// Clear terminal and error highlights at the start of each run
term.write('\x1b[2J\x1b[3J\x1b[H');
if (window._clearErrorHighlights) window._clearErrorHighlights();
// Remove any stale socket listeners from previous runs
socket.removeAllListeners('execution_complete');
// Use the current run mode
if (currentRunMode === 'run') {
await runProgram({ silent: false });
} else if (currentRunMode === 'syntax') {
await runSyntax({ silent: false });
} else if (currentRunMode === 'semantic') {
await runSemantic({ silent: false });
} else {
await runLexer({ silent: false });
}
};
window.selectRunMode = function(mode) {
currentRunMode = mode;
const modeText = mode.charAt(0).toUpperCase() + mode.slice(1);
document.getElementById('run-mode-text').textContent = modeText;
document.getElementById('run-dropdown-menu').classList.add('hidden');
};
window.toggleRunDropdown = function(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
const menu = document.getElementById('run-dropdown-menu');
if (menu) {
menu.classList.toggle('hidden');
// Position dropdown correctly on mobile
if (!menu.classList.contains('hidden') && window.innerWidth <= 768) {
const btn = document.querySelector('.btn-dropdown-toggle');
if (btn) {
const rect = btn.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = (rect.bottom + 4) + 'px';
menu.style.right = (window.innerWidth - rect.right) + 'px';
menu.style.left = 'auto';
}
}
}
};
// Add touch support for dropdown toggle
const dropdownToggleBtn = document.querySelector('.btn-dropdown-toggle');
if (dropdownToggleBtn) {
dropdownToggleBtn.addEventListener('touchend', function(e) {
e.preventDefault();
window.toggleRunDropdown(e);
}, { passive: false });
}
// Add touch support for dropdown items
document.querySelectorAll('.run-dropdown-item').forEach(function(item) {
item.addEventListener('touchend', function(e) {
e.preventDefault();
const mode = this.getAttribute('data-mode');
if (mode) {
window.selectRunMode(mode);
}
}, { passive: false });
});
});
});
document.querySelector(".widthResizer").addEventListener("mousedown", (e) => {
e.preventDefault();
document.addEventListener("mousemove", widthResize);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", widthResize);
}, { once: true });
});
function widthResize(e) {
let newWidth = e.clientX - document.querySelector(".textFieldCont").getBoundingClientRect().left;
document.querySelector(".textFieldCont").style.width = `${newWidth}px`;
}
/* ── Height resizer for terminal ── */
const heightResizer = document.querySelector('.heightResizer');
if (heightResizer) {
const workspaceMain = document.querySelector('.workspace-main');
const mainCont = document.querySelector('.mainCont');
const termCont = document.querySelector('.terminalCont');
heightResizer.addEventListener('mousedown', (e) => {
e.preventDefault();
heightResizer.classList.add('active');
document.body.style.cursor = 'ns-resize';
document.body.style.userSelect = 'none';
function onMouseMove(ev) {
const parentRect = workspaceMain.getBoundingClientRect();
const resizerH = heightResizer.offsetHeight;
// y position relative to workspace-main
const y = ev.clientY - parentRect.top;
const minEditor = 100;
const minTerminal = 100;
const available = parentRect.height - resizerH;
let editorH = Math.max(minEditor, Math.min(y, available - minTerminal));
let termH = available - editorH;
mainCont.style.height = editorH + 'px';
termCont.style.height = termH + 'px';
// Re-fit xterm
if (window.fitAddon) window.fitAddon.fit();
}
function onMouseUp() {
heightResizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (window.fitAddon) window.fitAddon.fit();
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Touch support for mobile
heightResizer.addEventListener('touchstart', (e) => {
e.preventDefault();
heightResizer.classList.add('active');
function onTouchMove(ev) {
const touch = ev.touches[0];
const parentRect = workspaceMain.getBoundingClientRect();
const resizerH = heightResizer.offsetHeight;
const y = touch.clientY - parentRect.top;
const minEditor = 100;
const minTerminal = 100;
const available = parentRect.height - resizerH;
let editorH = Math.max(minEditor, Math.min(y, available - minTerminal));
let termH = available - editorH;
mainCont.style.height = editorH + 'px';
termCont.style.height = termH + 'px';
if (window.fitAddon) window.fitAddon.fit();
}
function onTouchEnd() {
heightResizer.classList.remove('active');
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
if (window.fitAddon) window.fitAddon.fit();
}
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
}, { passive: false });
}
/* ── Sidebar resizer (drag to resize lexeme panel) ── */
const sidebarResizer = document.querySelector('.sidebarResizer');
if (sidebarResizer) {
const sidebar = document.querySelector('.sidebar');
sidebarResizer.addEventListener('mousedown', (e) => {
e.preventDefault();
sidebarResizer.classList.add('active');
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
function onMouseMove(ev) {
const workspaceRect = document.querySelector('.workspace').getBoundingClientRect();
const newWidth = ev.clientX - workspaceRect.left - 12; // 12 = workspace padding
const clamped = Math.max(200, Math.min(newWidth, workspaceRect.width * 0.7));
sidebar.style.width = clamped + 'px';
// Trigger Monaco editor relayout
if (window.editor && window.editor.layout) window.editor.layout();
}
function onMouseUp() {
sidebarResizer.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (window.editor && window.editor.layout) window.editor.layout();
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Touch support
sidebarResizer.addEventListener('touchstart', (e) => {
e.preventDefault();
sidebarResizer.classList.add('active');
function onTouchMove(ev) {
const touch = ev.touches[0];
const workspaceRect = document.querySelector('.workspace').getBoundingClientRect();
const newWidth = touch.clientX - workspaceRect.left - 12;
const clamped = Math.max(200, Math.min(newWidth, workspaceRect.width * 0.7));
sidebar.style.width = clamped + 'px';
if (window.editor && window.editor.layout) window.editor.layout();
}
function onTouchEnd() {
sidebarResizer.classList.remove('active');
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
if (window.editor && window.editor.layout) window.editor.layout();
}
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
}, { passive: false });
}
function toggleDropdown() {
const menu = document.getElementById("dropdown-menu");
menu.classList.toggle("hidden");
}
// Hide dropdown if clicked outside (handles both click and touch)
function hideDropdownsOnOutsideClick(e) {
const dropdown = document.querySelector(".dropdown");
const menu = document.getElementById("dropdown-menu");
if (dropdown && menu && !dropdown.contains(e.target)) {
menu.classList.add("hidden");
}
// Hide run dropdown if clicked outside
const runDropdown = document.querySelector(".run-dropdown");
const runMenu = document.getElementById("run-dropdown-menu");
if (runDropdown && runMenu && !runDropdown.contains(e.target)) {
runMenu.classList.add("hidden");
}
}
document.addEventListener("click", hideDropdownsOnOutsideClick);
document.addEventListener("touchstart", hideDropdownsOnOutsideClick, { passive: true });
// Auto-lex on typing (debounced) to keep sidebar tokens in sync with GALalexer rules
function debounce(fn, wait){
let t; return function(...args){ clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait); };
}
const debouncedLex = debounce(() => window.runLexer({ silent: true }), 400);
if (window.editor && editor.onDidChangeModelContent){
editor.onDidChangeModelContent(() => {
debouncedLex();
});
}