| |
| let fitAddon; |
| document.addEventListener('DOMContentLoaded', async () => { |
|
|
| |
| const API_BASE = (location.port && location.port !== '5000') ? 'http://localhost:5000' : ''; |
|
|
| |
| 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; |
| let waitingForInput = false; |
| let userInput = ''; |
| |
| window._resetInputState = function() { waitingForInput = false; userInput = ''; }; |
| let inputCallback = null; |
| let variable = ''; |
| 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: [ |
| |
| [/\b(tree|seed|leaf|branch|vine)\b/, "type"], |
| |
| [/\b(variety|fertile|soil|bundle)\b/, "advancedType"], |
| |
| [/\b(plant|harvest|grow|prune|graft|water|sow|root|sprout|bud|cultivate|tend|skip|reclaim|pollinate)\b/, "control"], |
| |
| [/\b(sunshine|frost|tr|fs|empty)\b/, "boolean"], |
| |
| [/\b(bloom|wither|spring)\b/, "io"], |
| |
| [/\/\/.*/, "comment"], |
| [/\/\*/, 'comment', '@comment'], |
| |
| [/\d+\.\d+/, "number"], |
| [/\d+/, "number"], |
| |
| [/"[^"]*"/, "string"], |
| |
| [/'.'/, "character"], |
| |
| [/[+\-*/%<>=!&|~]+/, "operator"], |
| |
| [/\b[a-zA-Z_]\w*(?=\()/, "functionIdentifier"], |
| |
| [/\b[a-zA-Z_]\w*\b/, "identifier"], |
| |
| [/[\{\}]/, "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, |
| }, |
| |
| quickSuggestions: false, |
| parameterHints: { enabled: false }, |
| codeLens: false, |
| folding: false, |
| lineDecorationsWidth: 0, |
| lineNumbersMinChars: 3, |
| renderLineHighlight: 'line', |
| occurrencesHighlight: false, |
| selectionHighlight: false, |
| renderValidationDecorations: 'on', |
| }); |
|
|
| |
| 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' |
| } |
| } |
| ]); |
| }); |
|
|
| |
|
|
| |
| 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]; |
| |
| if (ch === '\n') { line++; col = 1; inLineComment = false; continue; } |
| |
| if (inBlockComment) { |
| if (ch === '*' && next === '/') { inBlockComment = false; i++; col += 2; } |
| else col++; |
| continue; |
| } |
| |
| if (inLineComment) { col++; continue; } |
| |
| if (inString) { if (ch === '"') inString = false; col++; continue; } |
| |
| if (inChar) { if (ch === "'") inChar = false; col++; continue; } |
| |
| if (ch === '/' && next === '/') { inLineComment = true; col++; continue; } |
| if (ch === '/' && next === '*') { inBlockComment = true; i++; col += 2; continue; } |
| |
| if (ch === '"') { inString = true; col++; continue; } |
| if (ch === "'") { inChar = true; col++; continue; } |
| |
| 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 }); |
| } |
| } |
| col++; |
| } |
| |
| 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); |
| } |
|
|
| |
| 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(); |
| |
| if (inBlockComment) { |
| if (trimmed.includes('*/')) inBlockComment = false; |
| continue; |
| } |
| if (trimmed.startsWith('/*')) { |
| if (!trimmed.includes('*/')) inBlockComment = true; |
| continue; |
| } |
| |
| if (!trimmed || trimmed.startsWith('//')) continue; |
| |
| const codePart = trimmed.replace(/\/\/.*$/, '').trimEnd(); |
| if (!codePart) continue; |
| |
| if (/[{}]\s*$/.test(codePart)) continue; |
| |
| if (/^[{}]+$/.test(codePart)) continue; |
| |
| if (/^(variety|soil)\b/.test(codePart) && codePart.endsWith(':')) continue; |
| |
| if (/^(cultivate|grow|tend|spring|bud|pollinate|root)\b.*\)\s*$/.test(codePart)) continue; |
| |
| if (!codePart.endsWith(';') && !codePart.endsWith(':') && !codePart.endsWith('{') && !codePart.endsWith('}')) { |
| const lineNum = i + 1; |
| |
| 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); |
| |
| 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(); |
| }); |
| |
| updateBracketHighlights(); |
| updateSemicolonWarnings(); |
|
|
| |
| let errorDecCollection = editor.createDecorationsCollection([]); |
|
|
| |
| |
| |
| 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; |
| } |
| |
| 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); |
| |
| 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); |
| |
| 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; |
| window.fitAddon = fitAddon; |
| |
| setTimeout(() => term.focus(), 100); |
| term.write('Terminal Ready\r\n'); |
|
|
| fitAddon.fit(); |
| |
| window.addEventListener('resize', () => { |
| fitAddon.fit(); |
| }); |
|
|
| |
|
|
| |
| const btnRun = document.getElementById('btn-run'); |
| const btnLex = document.getElementById('btn-lex'); |
| |
|
|
| |
| 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()); |
| } |
| |
| 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()); |
| } |
|
|
| |
| 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(); |
| }); |
| } |
|
|
| |
|
|
| |
| 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 ? '▲' : '▼'; |
| } |
| }); |
| |
| |
| if (closeBtn) { |
| closeBtn.addEventListener('click', closeLexemeTable); |
| } |
| |
| |
| if (backdrop) { |
| backdrop.addEventListener('click', closeLexemeTable); |
| } |
| } |
|
|
| |
| (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; } |
| } |
| }); |
| |
| 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) { |
| |
| 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 = ''; |
|
|
| |
| const displayType = (tok) => { |
| |
| if (tok.type === 'idf' || tok.type === 'id' || tok.type === 'TT_IDENTIFIER') { |
| return 'id'; |
| } |
|
|
| |
| 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'; |
|
|
| |
| if (tok.type === 'branch') return 't/f'; |
|
|
| |
| if (tok.type === 'sunshine') return 'sunshine'; |
| if (tok.type === 'frost') return 'frost'; |
| |
| |
| 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; |
|
|
| |
| 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; |
|
|
| |
| if (tok.type && tok.type.startsWith('TT_')) { |
| |
| if (typeof tok.value === 'string' && SYMBOLS.has(tok.value)) return tok.value; |
| return tok.type.substring(3).toLowerCase(); |
| } |
| return tok.type || ''; |
| }; |
|
|
| |
| 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 || ''; |
| |
| 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'; |
| |
| if (t === 'sunshine' || t === 'frost') return 'false'; |
| |
| const OPS = new Set(['+','-','*','/','%','=','==','+=','-=','*=','/=','%=','<','>','<=','>=','!=','&&','||','!','++','--','~','`']); |
| const lex = (tok.value == null ? '' : String(tok.value)); |
| if (OPS.has(lex) || OPS.has(t)) return 'operator'; |
| |
| if (SYMBOLS.has(lex)) return symbolTypeName(lex); |
| if (SYMBOLS.has(t)) return symbolTypeName(t); |
| |
| return ''; |
| }; |
| |
| |
| const visibleTokens = (data.tokens || []).filter(t => t && t.type !== 'TT_NL' && t.type !== 'TT_EOF' && t.type !== 'EOF'); |
| |
| |
| const operatorTokens = new Set(['+', '-', '*', '/', '%', '**', '~', '++', '--', |
| '=', '+=', '-=', '*=', '/=', '%=', '==', '!=', '<', '>', '<=', '>=', |
| '&&', '||', '!', '`']); |
| |
| visibleTokens.forEach(token => { |
| const vDisp = token.value == null ? '' : String(token.value); |
| const tDisp = displayType(token); |
| |
| 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; |
| } |
| }); |
|
|
| |
| |
| 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'); |
| } |
| |
| |
| 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); |
|
|
| |
| const sl = document.getElementById('status-lex'); |
| const ss = document.getElementById('status-syn'); |
| |
| |
| 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; |
| } |
| }; |
|
|
| |
| window.runSemantic = async function (options = {}) { |
| const silent = options.silent || false; |
| const sourceCode = editor.getValue(); |
| console.log("Running semantic analysis with source code:", sourceCode); |
| |
| |
| 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); |
| |
| |
| 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) { |
| |
| 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) { |
| |
| 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') { |
| |
| 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'); |
| } |
| }; |
|
|
| |
| |
| |
|
|
| |
| 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'; } |
| } |
| } |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| function runViaSocket(sourceCode, silent) { |
| |
| waitingForInput = false; |
| userInput = ''; |
| |
| window._runGeneration = (window._runGeneration || 0) + 1; |
| window._socketOutputLog = []; |
| |
| socket.removeAllListeners('execution_complete'); |
| |
| if (!socket.connected) { |
| socket.connect(); |
| } |
|
|
| |
| function doEmit() { |
| const myGen = window._runGeneration; |
| |
| socket.removeAllListeners('execution_complete'); |
| |
| socket.once('execution_complete', function onComplete(data) { |
| if (myGen !== window._runGeneration) return; |
| 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'); |
| } |
| } |
| } |
| |
| 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(); |
| }); |
| |
| 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(); |
|
|
| |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| |
| socket.removeAllListeners('execution_complete'); |
|
|
| |
| await runLexer({ silent: true }); |
|
|
| |
| 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: —'; } |
|
|
| |
| const needsInput = /\bwater\s*\(/.test(sourceCode); |
|
|
| if (needsInput) { |
| runViaSocket(sourceCode, silent); |
| } else { |
| await runViaREST(sourceCode, silent); |
| } |
| }; |
|
|
| |
| |
| window._runGeneration = 0; |
| window._socketOutputLog = []; |
| socket.on('output', function (data) { |
| |
| 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(); |
| }); |
|
|
| |
| 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'; } |
| } |
| }); |
| |
| |
| 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 = ''; |
| }); |
|
|
| |
| let currentRunMode = 'run'; |
| |
| window.runCode = async function () { |
| |
| term.write('\x1b[2J\x1b[3J\x1b[H'); |
| if (window._clearErrorHighlights) window._clearErrorHighlights(); |
| |
| socket.removeAllListeners('execution_complete'); |
| |
| |
| 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'); |
| |
| |
| 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'; |
| } |
| } |
| } |
| }; |
| |
| |
| const dropdownToggleBtn = document.querySelector('.btn-dropdown-toggle'); |
| if (dropdownToggleBtn) { |
| dropdownToggleBtn.addEventListener('touchend', function(e) { |
| e.preventDefault(); |
| window.toggleRunDropdown(e); |
| }, { passive: false }); |
| } |
| |
| |
| 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`; |
| } |
|
|
| |
| 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; |
| |
| 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'; |
|
|
| |
| 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); |
| }); |
|
|
| |
| 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 }); |
| } |
|
|
| |
| 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; |
| 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 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); |
| }); |
|
|
| |
| 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"); |
| } |
| |
|
|
| |
| |
| 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"); |
| } |
| |
| |
| 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 }); |
|
|
| |
| 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(); |
| }); |
| } |
|
|
|
|