I5 · Part 2 · How it runs
An editor inside a web page
CodeMirror, a worker full of tools, files that live in localStorage, and links that carry code.
10On this page
One component, three jobs
The playground, every problem page and the mock interview room all render the same component,
components/practice/PracticeWorkspace.tsx. At about 1,800 lines it is the second-largest component in
the repository, after the whiteboard, and it decides its shape from two flags. With isFree set and no interview it
is the playground: a project of files, a Live switch, share links and run history. Without it, it is
a problem page, with a brief on the left (Description, Tests and Hints tabs), a Submit button and
the solved state, stamped on the page with a pill to the next problem. Inside the mock interview it drops the navigation, the saved code and the
debugger.
Under the editor sits one output panel with up to seven tabs: Console, Debug, Problems, History, Input, Preview and Test Result. Which ones appear depends on the mode. History, Input and Preview belong to the playground; Test Result belongs to problems; Debug appears only after a debug run. On problem pages a drag handle resizes the editor between 220 and 900 pixels, and the height is saved.
CodeMirror, assembled by hand
The editor is CodeMirror 6 through @uiw/react-codemirror, with
basicSetup={false}. components/practice/CodeEditor.tsx lists every extension
itself: line numbers, history, bracket matching and closing, autocompletion, folding, rectangular
selection, search in a top panel, selection-match highlighting, and the inline-results field from
lib/editor/inline.ts.
Anything that can change after the editor exists goes in a compartment, and there are seven: language, lint, wrap, tab size, indent guides, Vim and minimap. Changing a setting reconfigures one compartment in a single transaction, so the document, the undo history and the cursor all survive. Vim keys and the minimap are dynamic imports; nobody downloads them until they are switched on.
Language support is lazy as well. lib/codeLanguages.ts knows 17 languages, and each
entry has a support() function that imports its grammar only when that language is
picked. Twelve use a Lezer grammar package. Kotlin, Swift, C#, Ruby and Lua use CodeMirror's legacy
stream modes, wrapped in StreamLanguage.define.
The tools worker
lib/editor/tools.ts owns one module worker and a map of pending promises keyed by a
counter. Each call posts { id, type, code, lang }, and the reply resolves whichever
promise has that id. The worker answers four request types.
- format imports Prettier's standalone build with the estree plugin and either the babel or the typescript parser, formats at a print width of 100 with the chosen tab width, and hands back the new cursor position so the caret stays where it was.
- lint for JavaScript runs
eslint-linter-browserifywith a flat config of 48 rules: 34 errors, 13 warnings and one turned off. The globals include the browser, worker and ES2021 sets, plus the site's ownassert,compare,readlineand__loopGuard. Where ESLint offers a fix, the diagnostic carries a Fix action. - lint for TypeScript is not ESLint at all. It builds a real
ts.createProgramover an in-memory host withstrict: true, fetching the 46 lib files from/wasm/typescript-lib/, and reports syntactic and semantic diagnostics with theirts(code)numbers. - instrument rewrites code for the step debugger, which has its own chapter.
The lint source waits for the browser to go idle before its first request (up to two seconds), so loading ESLint does not compete with the page's own start-up. After every reply it checks that the document and the language are still the ones it sent. If not, it returns nothing rather than painting squiggles on the wrong text. Formatting does the same check before it replaces the document.
Settings
lib/editor/settings.ts holds eight settings, saved together under one localStorage key
and merged over the defaults on load, so a setting added later still gets a value.
| Setting | Default | What it does |
|---|---|---|
| formatOnSave | on | Cmd/Ctrl+S runs Prettier first, for JavaScript and TypeScript |
| runOnSave | off | Cmd/Ctrl+S also runs the code |
| lint | on | ESLint or the type checker as you type |
| vim | off | Loads @replit/codemirror-vim |
| minimap | off | Loads @replit/codemirror-minimap |
| indentGuides | on | Indentation markers |
| wrap | off | Line wrapping |
| tabSize | 2 | 2 or 4; anything else saved is read back as 2 |
Text size (11 to 24 pixels, starting at 14.5) and fullscreen are component state, not settings, so they reset on reload.
The command palette
Cmd/Ctrl+Shift+P or F1 opens CommandPalette.tsx. It holds 20 fixed commands (run,
save, format, fix, next problem, go to line, find, fold, unfold, the view toggles, Vim, lint, the two
on-save switches, indent size, text size and fullscreen), three file commands in the playground,
and one command per language. That makes 40 in the playground and 35 on a problem page, where HTML
and CSS are not offered. Matching is a small scoring function: a direct substring scores
1,000 minus its position, and a scattered subsequence scores 500 minus the gaps between letters, so
"fmt" still finds "Format document".
Files and tabs
The playground is a project, defined in lib/playgroundProject.ts: a list of files, each
with an id, a name, a language and its code, plus the id of the active one. It lives in localStorage
under groundwork:playground:project. Typing saves it 300 ms after the last keystroke;
Cmd/Ctrl+S saves at once. A project that fails validation is replaced by a fresh one, and a first
load gathers any code saved by the older one-file-per-language playground into tabs, so nobody lost
work when files arrived.
Names are kept unique by adding -2, -3 and so on. Renaming a file to a new
extension switches its language, because langForName looks the extension up in the same
17-language table. Opening the HTML quick language creates index.html with a
style.css and a script.js beside it when they are missing.
Closing a file that has code in it asks first. A file counts as touched when it is not empty and is not exactly one of the 28 starter templates. Closed files can be brought back with Reopen for ten seconds.
Modals
New file, rename and every confirmation use components/Modal.tsx, which wraps the
native <dialog> element and opens it with showModal(). The browser
then provides the focus trap, the Escape key and the inert page behind it; a click on the backdrop
closes it too. ConfirmDialog puts focus on Cancel, so pressing Enter by reflex does not
delete anything. NameDialog trims names to 60 characters and validates as you type.
components/practice/FileDialogs.tsx builds the two file dialogs on top. New file shows
all 17 languages as a radio group and a live hint such as "Creates scratch.py, Python, runs here". A
typed extension wins over the picked language. Rename selects the name up to the extension and says
whether the new extension will switch the language. Both refuse slashes, a name that starts with a
dot, and a name another file already has, compared without case.
Share links
Share packs every file as a [name, language, code] triple, turns it into JSON,
compresses it with the browser's own CompressionStream("deflate-raw"), and writes it as
base64url into the fragment: /practice?id=free#share=…. The fragment never reaches a
server, so a shared program is stored nowhere except in the link. A link longer than 60,000
characters is refused as too big.
Opening a link reads it defensively in lib/shareLink.ts: at most 20 files, names cut to
60 characters, code to 200,000, and unknown languages dropped. The shared files are added as new
tabs next to the reader's own, and the fragment is removed from the address bar.
Run history, Live mode and inline results
Every playground run of a runnable file is recorded by lib/runHistory.ts: the time, the
file, its code, the duration, whether anything was logged as an error, and the first output line cut
to 80 characters. The newest 15 are kept in localStorage. The History tab reads them through
useSyncExternalStore, with a snapshot cached by the raw string so React sees a stable
value. "Open this code" brings an old run back as a new tab.
Live mode, for JavaScript, TypeScript, HTML and CSS, runs the code 700 ms after typing stops. After
any run in JavaScript, Python, Ruby, Lua, C or C++, groupByLine folds the console output
into one result per source line and showInline draws it at the end of that line as a
CodeMirror widget: the text cut to 90 characters, a count such as ×3 when the line ran
more than once, and errors taking priority over logs.
Each runtime finds the line differently. The JavaScript worker records the stack line of a marker
call placed just above the reader's code and subtracts it from each console.log's stack
line. Python's print walks up the frames to the reader's file. Ruby's
puts reads caller_locations. Lua's print asks
debug.getinfo. Those three put an invisible \x02line\x03 marker in
front of each printed line, which the worker strips before display. In C and C++ only compiler
diagnostics carry a line. TypeScript and PHP get no inline results.
The web preview
Running an HTML file, or a stylesheet or script one links to, calls buildPage in
lib/webPreview.ts. It replaces each <link href> and
<script src> that names a project file with the file's contents inline, escaping
any closing tag inside them, and puts a small bridge script at the top of the head. The bridge
forwards console calls, errors, unhandled rejections and the load event to the parent
with postMessage. The page renders in an iframe with srcdoc and
sandbox="allow-scripts allow-modals allow-forms". The parent accepts a message only when
it carries the preview's tag and comes from that iframe's window.
Nothing in this chapter talks to a server. Files, settings, run history and standard input are in
localStorage; a share link carries its code in the fragment. That keeps the editor free to run and
impossible to leak from, and it means clearing site data clears the playground. The
/problems/[slug]/cases file for grading other languages is the one exception, and it
is a static file made at build time.
Finished reading?
0 of 26 chapters read in this series