diff --git a/CLAUDE.md b/CLAUDE.md index 5764d09..14e115c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ Worker messages: - **Layout and scrolling:** nested boxes show their variables below their code (CSS). The scroll effect follows the active box horizontally. It sets `scrollLeft`/`scrollTop` on the visualizer view only; don't use `scrollIntoView`, which also scrolls the LiaScript page around the embed and moves the step buttons. - **Console and Stop:** output, REPL inputs, Tests reports and notes form one transcript (`ConsoleEntry` list after `output`). While Python is busy for more than `STOP_BUTTON_DELAY`, the run button is **Stop**: `stopPython` terminates the worker and calls `startWorker` again, so the REPL namespace is gone after a stop. The embed reports this to its parent (protocol version 4, `glpython:busy`/`glpython:stop`). - **One Monaco model for all files:** a tab switch replaces the editor's text. `updateCode` ignores flush events (`setValue`), since `@monaco-editor/react` shows a read-only file (`ProjectFile.readOnly`) with `setValue` before its `onChange` knows the new tab - the text would land in the previous file. +- **Unicode marks in the editor:** Monaco boxes look-alike and invisible characters (`EDITOR_UNICODE_HIGHLIGHT`), except the typographic ones of a German text (dashes, quotes, ×, an allow-list), also in strings and comments. Invisible ones stay marked everywhere: in a doctest's expected output a non-breaking space fails with an Expected/Got that look the same. - **UI strings** come in via the `t: CoreStrings` prop. `src/i18n.ts` is their single source: `coreStrings` holds German and English, and `resolveUiLocale` picks the language. The embed uses them directly; the glpython IDE imports them and adds only its shell strings. New strings have been added as *optional* `CoreStrings` keys with an English fallback, so older hosts keep compiling; add both languages to `coreStrings`. Python values, reprs and error messages are never translated. - **Two layouts:** with `compact` (the embed) the original tree (`main-split` > `editor-column` + graphics aside), unchanged and styled by `src/embed/style.css`; without it (the IDE) the stacked layout (`workspace-stack`: `stack-top` with the editor panel and, while drawing, the graphics, then `stack-divider` and `stack-console`). Both are assembled from the same JSX pieces (`actionsNode`, `visualizerBarNode`, `editorWrapNode`, `consoleNode`, …) just before the return. The buttons can be portaled into the host's bar (`actionsContainer`). - **Embedded/reduced layouts:** the knobs are `compact`, `capabilities` (`WorkspaceCapabilities`, including `repl`) and `command` (`WorkspaceCommand`, an immutable `{id, type}` used by hosts to trigger run/visualize/test/stop/reset without the workspace's own buttons; `reset` restarts Python and clears the console). Defaults must preserve the full-workspace behavior. diff --git a/src/GlPythonWorkspace.tsx b/src/GlPythonWorkspace.tsx index 8a1f6a0..e624562 100644 --- a/src/GlPythonWorkspace.tsx +++ b/src/GlPythonWorkspace.tsx @@ -317,6 +317,19 @@ type ConsoleEntry = | { kind: 'tests'; id: number; report: TestReport } | { kind: 'note'; id: number; text: string } +// Monaco boxes characters that look like others and invisible ones. Not the +// typographic ones of a German text (dashes, quotes, ×), which Python rejects +// in code with a clear SyntaxError anyway; invisible ones stay marked +// everywhere, also in strings and comments - in a doctest's expected output a +// non-breaking space fails with an "Expected"/"Got" that look the same. +const EDITOR_UNICODE_HIGHLIGHT = { + ambiguousCharacters: true, + invisibleCharacters: true, + includeStrings: true, + includeComments: true, + allowedCharacters: Object.fromEntries([...'\u2010\u2011\u2012\u2013\u2018\u2019\u201A\u201B\u2039\u203A\u00D7\u2032'].map((character) => [character, true])) as Record, +} + // The console keeps the last part of a long output: a program printing in an // endless loop must not fill the page's memory before it is stopped. const CONSOLE_MAX_LENGTH = 100_000 @@ -2088,7 +2101,7 @@ function GlPythonWorkspace(props: GlPythonWorkspaceProps) { const visualizerBarNode =
{visualTraceSteps.length > 0 ? <>
{ setVisualStepIndex(Number(event.target.value)) }} />{errorTraceStep && }
{clampedStepIndex === -1 ? t.visualizerBeforeStart : t.visualizerStepOf(clampedStepIndex + 1, visualTraceSteps.length)}{visualTrace?.truncated && {t.visualizerTruncated}} : {t.visualizerNoSteps}}
{workspaceCapabilities.export && <>}{workspaceCapabilities.exitVisualizer && }
const visualizerWarningNode = <>{isVisualizing && visualTrace?.error && !errorTraceStep &&
{t.visualizerError(visualTrace.error)}
} const editorTabsNode =
{openFiles.map((path) => { const file = project.files[path]; return })}
- const editorWrapNode =
{isVisualizing ?
: null} />{animationsEnabled && closingBoxes.length > 0 &&
{closingBoxes.map((box) =>
)}
}{(connectorPaths.length > 0 || flights.length > 0) && {connectorPaths.map((connector, index) => )}{animationsEnabled && flights.map((flight) => )}}{animationsEnabled && flights.length > 0 &&
{flights.map((flight) => {flight.value})}
}
: { registerGlpythonPython(instance); defineGlpythonTheme(instance) }} value={selectedFile.code} onChange={updateCode} onMount={setNormalEditor} options={{ minimap: { enabled: false }, bracketPairColorization: { enabled: false }, fontSize: 15, lineHeight: 24, padding: { top: 22 }, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', scrollBeyondLastLine: false, smoothScrolling: true, automaticLayout: true, tabSize: 4, insertSpaces: true, detectIndentation: false, placeholder: t.editorPlaceholder ?? 'Type code here', readOnly: selectedFile.readOnly === true, domReadOnly: selectedFile.readOnly === true, readOnlyMessage: { value: t.readOnlyFile ?? 'Read-only: this file can be read but not changed.' } }} />}
+ const editorWrapNode =
{isVisualizing ?
: null} />{animationsEnabled && closingBoxes.length > 0 &&
{closingBoxes.map((box) =>
)}
}{(connectorPaths.length > 0 || flights.length > 0) && {connectorPaths.map((connector, index) => )}{animationsEnabled && flights.map((flight) => )}}{animationsEnabled && flights.length > 0 &&
{flights.map((flight) => {flight.value})}
}
: { registerGlpythonPython(instance); defineGlpythonTheme(instance) }} value={selectedFile.code} onChange={updateCode} onMount={setNormalEditor} options={{ minimap: { enabled: false }, bracketPairColorization: { enabled: false }, fontSize: 15, lineHeight: 24, padding: { top: 22 }, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', scrollBeyondLastLine: false, smoothScrolling: true, automaticLayout: true, tabSize: 4, insertSpaces: true, detectIndentation: false, placeholder: t.editorPlaceholder ?? 'Type code here', unicodeHighlight: EDITOR_UNICODE_HIGHLIGHT, readOnly: selectedFile.readOnly === true, domReadOnly: selectedFile.readOnly === true, readOnlyMessage: { value: t.readOnlyFile ?? 'Read-only: this file can be read but not changed.' } }} />}
const consoleNode = <>{!hideOutputPanel &&
{t.output}{runtimeState !== 'ready' && {t.pyodideRuntime}}
{Object.keys(project.files).length > 1 && {t.runsFile(project.mainFile)}}
{ const element = event.currentTarget; consoleAtBottomRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 24 }}>{isVisualizing ?
{isAtErrorStep ? <>{visualStdout && !visualStdout.endsWith('\n') ? `${visualStdout}\n` : visualStdout}{visualTrace?.error} : visualStdout || t.noOutput}
: <>
{output}
{consoleEntries.map((entry) => entry.kind === 'tests' ?
: entry.kind === 'note' ?

{entry.text}

:
 visualizeReplEntry(entry.id) : undefined} busy={isBusy} />
)}}
{workspaceCapabilities.repl &&
{ event.preventDefault(); submitRepl() }}> { setReplInput(event.target.value); setReplHistoryIndex(null) }} onKeyDown={handleReplKeyDown} aria-label={t.replInputLabel ?? 'Python input'} spellCheck={false} autoComplete="off" autoCapitalize="off" />
}
} // In the stacked layout the graphics show only while the program draws - // for a visualization: if it draws at all, so the panel doesn't appear in