C#
Hello, World!
●
CoolCalculatorCS/Hello.cs
⟲ Reset
▶ Run
⌘↵
CS
Console — Program.exe
.NET 6.0
Press ▶ Run to see the expected output for this lesson.
Output is pre-baked for the unedited lesson — actually compile your edits in dotnetfiddle.net ↗
⎘ Copy
↗ Fiddle
CS' + '
' + entry.label + '
' + '
' + entry.files.length + 'f
'; btn.addEventListener('click', () => activate(entry.id)); li.appendChild(btn); return li; } (function renderSteps(){ const groups = []; const seen = new Map(); STEPS.forEach(s => { const g = s.group || 'Other'; if(!seen.has(g)){ seen.set(g, { name:g, items:[] }); groups.push(seen.get(g)); } seen.get(g).items.push(s); }); groups.forEach(g => { const head = document.createElement('li'); head.className = 'file-group-head'; head.textContent = g.name; fileListEl.appendChild(head); g.items.forEach(p => fileListEl.appendChild(stepButtonNode(p))); }); })(); searchEl.addEventListener('input', ()=>{ const q = searchEl.value.toLowerCase().trim(); document.querySelectorAll('.file-btn').forEach(b=>{ const id = b.dataset.step; const label = b.querySelector('.label').textContent.toLowerCase(); const match = !q || id.toLowerCase().includes(q) || label.includes(q); b.parentElement.style.display = match ? '' : 'none'; }); document.querySelectorAll('.file-group-head').forEach(head => { let n = head.nextElementSibling, anyVisible = false; while(n && !n.classList.contains('file-group-head')){ if(n.style.display !== 'none'){ anyVisible = true; break; } n = n.nextElementSibling; } head.style.display = anyVisible ? '' : 'none'; }); }); function modeForFile(name){ if(name.endsWith('.cs')) return 'text/x-csharp'; if(name.endsWith('.txt')) return 'null'; return 'text/x-csharp'; } function badgeForFile(name){ if(name.endsWith('.cs')) return 'CS'; if(name.endsWith('.txt')) return 'TXT'; return 'CS'; } function activeStep(){ return STEPS.find(s => s.id === state.active); } function isDirty(file){ return state.buffers[file] != null && state.originals[file] != null && state.buffers[file] !== state.originals[file]; } function refreshDirty(){ const step = activeStep(); if(!step) return; const dirty = step.files.some(f => isDirty(f)); const btn = document.querySelector('.file-btn[data-step="' + step.id + '"]'); if(btn) btn.classList.toggle('dirty', dirty); fileNameEl.classList.toggle('dirty', dirty); renderFileTabs(); } function renderFileTabs(){ const step = activeStep(); if(!step){ fileTabsEl.innerHTML = ''; return; } const cur = state.activeFile[step.id] || step.entry; const esc = (s) => String(s).replace(/&/g,'&').replace(/ { const display = f.split('/').pop(); // tab label = basename const isEntry = f === step.entry; const cls = ['file-tab']; if(f === cur) cls.push('active'); if(isEntry) cls.push('entry'); if(isDirty(f)) cls.push('dirty'); return '
' + esc(display) + '
'; }).join(''); fileTabsEl.querySelectorAll('.file-tab').forEach(b => { b.addEventListener('click', () => switchFile(b.dataset.file)); }); const activeBtn = fileTabsEl.querySelector('.file-tab.active'); if(activeBtn && activeBtn.scrollIntoView){ try { activeBtn.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch {} } } fileTabsEl.addEventListener('wheel', (e) => { if(fileTabsEl.scrollWidth <= fileTabsEl.clientWidth) return; const delta = Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX; if(delta === 0) return; e.preventDefault(); fileTabsEl.scrollLeft += delta; }, { passive: false }); function loadEditorFor(file){ const step = activeStep(); if(!step) return; ensureLoaded(file); editor.setOption('mode', modeForFile(file)); editor.setValue(getFileText(file)); editor.clearHistory(); modeBadgeEl.textContent = badgeForFile(file); fileSubEl.textContent = 'CoolCalculatorCS/' + file; fileNameEl.firstChild.textContent = step.label.replace(/^\d+\s*·\s*/, ''); } function switchFile(file){ const step = activeStep(); if(!step) return; const cur = state.activeFile[step.id] || step.entry; state.buffers[cur] = editor.getValue(); state.activeFile[step.id] = file; loadEditorFor(file); refreshDirty(); editor.focus(); } function activate(stepId){ const prior = activeStep(); if(prior){ const cur = state.activeFile[prior.id] || prior.entry; state.buffers[cur] = editor.getValue(); refreshDirty(); } state.active = stepId; document.querySelectorAll('.file-btn').forEach(b => b.classList.toggle('active', b.dataset.step === stepId)); const step = activeStep(); step.files.forEach(f => ensureLoaded(f)); if(!state.activeFile[step.id]) state.activeFile[step.id] = step.entry; loadEditorFor(state.activeFile[step.id]); refreshDirty(); consoleTitle.textContent = step.title || 'Console'; consoleOut.classList.remove('calc-mode'); consoleOut.innerHTML = step.id === 'cool' ? '
Press ▶ Run to launch the interactive CoolCalculator.
' : '
Press ▶ Run to see the expected output for this lesson.
'; editor.focus(); } editor.on('change', ()=>{ const step = activeStep(); if(!step) return; const cur = state.activeFile[step.id] || step.entry; const value = editor.getValue(); state.buffers[cur] = value; // Only persist when the user has actually diverged from the embedded // original — `editor.setValue(original)` (run on every step open) would // otherwise silently snapshot the lesson and shadow future updates. if(value !== state.originals[cur]) saveBuffer(cur, value); else clearBuffer(cur); refreshDirty(); }); /* ========================================================== Run / Reset / Copy / Fiddle ========================================================== */ function escapeHtml(s){ return String(s) .replace(/&/g,'&').replace(//g,'>') .replace(/"/g,'"').replace(/'/g,'''); } function runStep(){ const step = activeStep(); if(!step) return; // The final-project step gets a real interactive calculator instead of a // pre-baked transcript — same UX as the C# console app. if(step.id === 'cool'){ mountCoolCalculator(consoleOut); return; } consoleOut.classList.remove('calc-mode'); const out = getExpectedOutput(step.outputFile); consoleOut.innerHTML = '
$
dotnet run\n' + escapeHtml(out).replace(/\n/g, '
') + '
'; consoleOut.scrollTop = 0; } /* ========================================================== Interactive CoolCalculator — JS port of the C# project that ships in step 13. Lives entirely inside the output panel. Supports +, -, *, / out of the box, runtime-added custom operators (`add` flow), and a 5-entry history rendered next to the ASCII art — exactly like the real .NET console app. ========================================================== */ const CALC_ART = ' _____________________\n' + '| _________________ |\n' + '| | CALCULATOR 0. | |\n' + '| |_________________| |\n' + '| ___ ___ ___ ___ |\n' + '| | 7 | 8 | 9 | | + | |\n' + '| |___|___|___| |___| |\n' + '| | 4 | 5 | 6 | | - | |\n' + '| |___|___|___| |___| |\n' + '| | 1 | 2 | 3 | | x | |\n' + '| |___|___|___| |___| |\n' + '| | . | 0 | = | | / | |\n' + '| |___|___|___| |___| |\n' + '|_____________________|'; function mountCoolCalculator(outEl){ const calc = { knownOps: ['+', '-', '*', '/'], customOps: {}, // symbol -> (x,y) => number history: [], // most-recent first, capped at 5 maxHistory: 5, mode: 'normal', // 'normal' | 'add-symbol' | 'add-logic' pendingSymbol: null, log: [], }; function escapeForCharClass(c){ return c.replace(/[\]\\^-]/g, '\\$&'); } function validate(operation){ if(!operation || operation.length === 0) return false; const opChars = calc.knownOps.map(escapeForCharClass).join(''); const re = new RegExp('^-?\\d*(\\.\\d+)?\\s*[' + opChars + ']\\s*-?\\d*(\\.\\d+)?$'); return re.test(operation.trim()); } function calculate(operation){ if(!validate(operation)) return null; // Match the C# behaviour: scan the known-ops list and keep the LAST // operator that's present in the string. let mathOp = null; for(const c of calc.knownOps){ if(operation.indexOf(c) >= 0) mathOp = c; } if(mathOp == null) return null; const parts = operation.split(mathOp).map(s => s.trim()).filter(s => s.length); if(parts.length !== 2) return null; const a = parseFloat(parts[0]); const b = parseFloat(parts[1]); if(isNaN(a) || isNaN(b)) return null; const customOp = calc.customOps[mathOp]; if(customOp){ try { return customOp(a, b); } catch { return null; } } switch(mathOp){ case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b; default: return null; } } function append(text, kind){ calc.log.push({ text: text, kind: kind || 'info' }); } function process(input){ const trimmed = input.trim(); if(calc.mode === 'normal'){ append(input, 'user'); if(trimmed === 'quit' || trimmed === 'exit'){ append('(calculator stopped — click ▶ Run again to restart)', 'system'); inputEl.disabled = true; return; } if(trimmed === 'add'){ append("Enter the symbol for the new operation (e.g. '%', '>'):", 'prompt'); calc.mode = 'add-symbol'; return; } const result = calculate(trimmed); if(result == null){ append('Could not calculate the answer.', 'error'); } else { // Trim trailing zeros for cleaner display ("4" instead of "4") const r = Number.isFinite(result) ? (Math.round(result * 1e8) / 1e8).toString() : String(result); append('Answer: ' + r, 'answer'); calc.history.unshift(trimmed + ' = ' + r); if(calc.history.length > calc.maxHistory) calc.history.pop(); } return; } if(calc.mode === 'add-symbol'){ append(input, 'user'); const symbol = trimmed; if(!symbol || symbol.length !== 1){ append('Invalid symbol. Please provide a single character.', 'error'); calc.mode = 'normal'; return; } if(calc.customOps[symbol] || calc.knownOps.indexOf(symbol) >= 0){ append("Operation '" + symbol + "' already exists.", 'error'); calc.mode = 'normal'; return; } calc.pendingSymbol = symbol; append('Enter the logic for the operation as a mathematical expression:', 'prompt'); append("Use 'x' and 'y' for the operands (e.g., 'x * y + 10'):", 'prompt'); calc.mode = 'add-logic'; return; } if(calc.mode === 'add-logic'){ append(input, 'user'); const symbol = calc.pendingSymbol; const logic = input; try { // The C# version uses DataTable.Compute on the substituted string; // here we just compile the expression to a 2-arg JS function. Same // result for the simple math expressions the lab supports. const fn = new Function('x', 'y', 'return (' + logic + ');'); const test = fn(2, 3); if(typeof test !== 'number' || !isFinite(test)){ throw new Error('expression did not produce a finite number'); } calc.customOps[symbol] = fn; if(calc.knownOps.indexOf(symbol) < 0) calc.knownOps.push(symbol); append("Custom operation '" + symbol + "' added successfully!", 'system'); append("Symbol '" + symbol + "' added to known operations!", 'system'); } catch(e) { append('Failed to add the operation. Error: ' + e.message, 'error'); } calc.pendingSymbol = null; calc.mode = 'normal'; return; } } // Build the calculator art with the history column on the right. function renderArt(){ const lines = CALC_ART.split('\n'); const out = []; if(calc.history.length === 0){ out.push(lines[0]); out.push(lines[1] + '\tNo history.'); for(let i = 2; i < lines.length; i++) out.push(lines[i]); } else { out.push(lines[0]); out.push(lines[1] + '\tHistory:'); for(let i = 2; i < lines.length; i++){ const opIdx = i - 2; if(opIdx < calc.history.length) out.push(lines[i] + '\t' + calc.history[opIdx]); else out.push(lines[i]); } } return out.join('\n'); } function render(){ artEl.textContent = renderArt(); if(calc.mode === 'normal'){ promptEl.textContent = "Please enter an operation or type 'add' to create a custom operation:"; } else if(calc.mode === 'add-symbol'){ promptEl.textContent = "Adding new operator — type a single-character symbol:"; } else if(calc.mode === 'add-logic'){ promptEl.textContent = "Adding new operator '" + calc.pendingSymbol + "' — type the math expression using x and y:"; } logEl.innerHTML = ''; for(const entry of calc.log){ const div = document.createElement('div'); div.className = 'calc-line ' + entry.kind; div.textContent = entry.text; logEl.appendChild(div); } logEl.scrollTop = logEl.scrollHeight; } // Build the DOM outEl.classList.add('calc-mode'); outEl.innerHTML = ''; const artEl = document.createElement('pre'); artEl.className = 'calc-art'; outEl.appendChild(artEl); const promptEl = document.createElement('div'); promptEl.className = 'calc-prompt-line'; outEl.appendChild(promptEl); const logEl = document.createElement('div'); logEl.className = 'calc-log'; outEl.appendChild(logEl); const row = document.createElement('div'); row.className = 'calc-input-row'; const sym = document.createElement('span'); sym.className = 'calc-prompt-sym'; sym.textContent = '>'; row.appendChild(sym); const inputEl = document.createElement('input'); inputEl.type = 'text'; inputEl.className = 'calc-input'; inputEl.placeholder = "e.g. 7 + 5 · add · exit"; inputEl.spellcheck = false; inputEl.autocomplete = 'off'; row.appendChild(inputEl); outEl.appendChild(row); // Tiny up/down command history for ergonomics const cmdHist = []; let cmdIdx = -1; inputEl.addEventListener('keydown', (e) => { if(e.key === 'Enter'){ const value = inputEl.value; if(value.length){ cmdHist.push(value); cmdIdx = cmdHist.length; } inputEl.value = ''; process(value); render(); setTimeout(() => inputEl.focus(), 0); } else if(e.key === 'ArrowUp'){ if(cmdIdx > 0){ cmdIdx--; inputEl.value = cmdHist[cmdIdx]; } e.preventDefault(); } else if(e.key === 'ArrowDown'){ if(cmdIdx < cmdHist.length - 1){ cmdIdx++; inputEl.value = cmdHist[cmdIdx]; } else { cmdIdx = cmdHist.length; inputEl.value = ''; } e.preventDefault(); } }); render(); setTimeout(() => inputEl.focus(), 50); } document.getElementById('runBtn').addEventListener('click', runStep); window.addEventListener('keydown', (e)=>{ if((e.metaKey || e.ctrlKey) && e.key === 'Enter'){ e.preventDefault(); runStep(); } }); document.getElementById('resetBtn').addEventListener('click', ()=>{ const step = activeStep(); if(!step) return; step.files.forEach(f => { if(state.originals[f] != null) state.buffers[f] = state.originals[f]; clearBuffer(f); }); loadEditorFor(state.activeFile[step.id] || step.entry); refreshDirty(); toast('Reset ' + step.id); }); document.getElementById('copyBtn').addEventListener('click', async ()=>{ const code = editor.getValue(); try { await navigator.clipboard.writeText(code); toast('Copied ' + code.split('\n').length + ' lines to clipboard'); } catch { // Fallback for browsers that block clipboard without explicit gesture const ta = document.createElement('textarea'); ta.value = code; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); toast('Copied to clipboard'); } }); document.getElementById('fiddleBtn').addEventListener('click', ()=>{ // dotnetfiddle.net's URL doesn't accept code via params reliably, so we // just open the site — the user pastes from the clipboard. Hint at that // by showing the toast. toast('Opening dotnetfiddle.net — paste your copied code'); window.open('https://dotnetfiddle.net/', '_blank', 'noopener'); }); /* ========================================================== Toast ========================================================== */ const toastEl = document.getElementById('toast'); let toastTimer; function toast(msg, kind){ toastEl.textContent = msg; toastEl.classList.toggle('error', kind === 'error'); toastEl.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(()=> toastEl.classList.remove('show'), 1800); } /* ========================================================== Drag-to-resize splitter ========================================================== */ (function initSplitter(){ const pane = document.querySelector('.editor-pane'); const splitter = document.getElementById('splitter'); if(!pane || !splitter) return; const HANDLE_W = 6, MIN_PANE = 160; const STORAGE = 'cs-lab-split-ratio'; let savedRatio = null; try { const v = parseFloat(localStorage.getItem(STORAGE)); if(v > 0 && v < 1) savedRatio = v; } catch {} function applyRatio(ratio){ const total = pane.clientWidth - parseFloat(getComputedStyle(pane).paddingLeft) * 2 - HANDLE_W - 14 * 2; if(total <= 0) return; const left = Math.max(MIN_PANE, Math.min(total - MIN_PANE, total * ratio)); pane.style.setProperty('--col-1', left + 'px'); pane.style.setProperty('--col-2', (total - left) + 'px'); if(typeof editor !== 'undefined' && editor.refresh){ clearTimeout(applyRatio._t); applyRatio._t = setTimeout(() => editor.refresh(), 30); } } if(savedRatio != null) applyRatio(savedRatio); let drag = null; splitter.addEventListener('pointerdown', (e) => { e.preventDefault(); const padL = parseFloat(getComputedStyle(pane).paddingLeft) || 0; drag = { paneLeft: pane.getBoundingClientRect().left + padL, total: pane.clientWidth - padL * 2 - HANDLE_W - 14 * 2 }; splitter.classList.add('active'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }); window.addEventListener('pointermove', (e) => { if(!drag) return; let editorPx = e.clientX - drag.paneLeft; editorPx = Math.max(MIN_PANE, Math.min(drag.total - MIN_PANE, editorPx)); applyRatio(editorPx / drag.total); }); window.addEventListener('pointerup', () => { if(!drag) return; drag = null; splitter.classList.remove('active'); document.body.style.cursor = ''; document.body.style.userSelect = ''; const total = pane.clientWidth - parseFloat(getComputedStyle(pane).paddingLeft) * 2 - HANDLE_W - 14 * 2; const colL = parseFloat(pane.style.getPropertyValue('--col-1')) || total / 2; try { localStorage.setItem(STORAGE, (colL / total).toFixed(4)); } catch {} }); splitter.addEventListener('dblclick', () => { pane.style.removeProperty('--col-1'); pane.style.removeProperty('--col-2'); try { localStorage.removeItem(STORAGE); } catch {} if(typeof editor !== 'undefined' && editor.refresh) setTimeout(() => editor.refresh(), 30); }); })(); /* ========================================================== Boot ========================================================== */ (function boot(){ STEPS.forEach(s => s.files.forEach(f => ensureLoaded(f))); activate(STEPS[0].id); })();