diff --git a/package.json b/package.json index 555fa95a..4e56b13c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:bridge": "node test/bridge.js", "test:outlines": "node test/outlines.js", "test:navigator": "node test/navigator.js", + "test:navigatoropencode": "node test/navigator-open-code.js", "test:classattr": "node test/class-attr.js", "test:gridtracks": "node test/grid-tracks.js", "test:windowbounds": "node test/window-bounds.js", diff --git a/src/App.tsx b/src/App.tsx index 07c1a1b3..094eda14 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,6 +100,12 @@ import { toRecord } from '../shared/record'; import { projectRelativePath } from './projectPath.js'; import { currentDesktopPlatform, shortcutLabel } from './shortcutLabel.js'; import { sourceNodeAtOffset } from './codePanelModel.js' +import { + codeWindowFor, + FRONTMATTER_SUBJECT, + type CodeSubject, + type FrontmatterSubject, +} from './codeWindowTarget'; import { cloneEditorModel, findEditorNodeById as findNodeById, @@ -3592,7 +3598,7 @@ export default function App() { }; const selectedNode: - | EditorNode | { readonly id: 'frontmatter'; readonly kind: 'frontmatter'; readonly value: string } | null = + | EditorNode | (FrontmatterSubject & { readonly value: string }) | null = model && selectedId ? selectedId === 'frontmatter' ? { id: 'frontmatter', kind: 'frontmatter', value: frontmatterCode } @@ -3944,29 +3950,22 @@ export default function App() { ? codeWinNode.inner : null; - // Returns whether the selection actually has a code editor, so the Enter + // Returns whether the subject actually has a code editor, so the Enter // shortcut below knows whether it handled the key. - const openCodeWindow = () => { - if (!selectedNode) { + const openCodeWindowFor = (subject: CodeSubject | null): boolean => { + const codeWindow = subject === null ? undefined : codeWindowFor(subject); + if (codeWindow === undefined) { return false; } - if (selectedNode.kind === 'frontmatter') { - setCodeWin({ - targetId: 'frontmatter', - title: 'Frontmatter', - language: 'javascript', - }); - return true; - } - if (selectedNode.kind === 'raw') { - setCodeWin({ - targetId: selectedNode.id, - title: `<${selectedNode.name}>`, - language: selectedNode.name === 'style' ? 'css' : 'javascript', - }); - return true; - } - return false; + setCodeWin(codeWindow); + return true; + }; + const openCodeWindow = (): boolean => openCodeWindowFor(selectedNode); + // A navigator double-click names its row instead of reading `selectedNode` + // (see `openCode` in StructureTree). An id the tree no longer holds, a row + // removed between the click and the render, opens nothing. + const openCodeWindowById = (id: string): void => { + openCodeWindowFor(id === 'frontmatter' ? FRONTMATTER_SUBJECT : tree.node(id)); }; // Read by the keydown effect, which is set up long before this exists. openCodeWindowRef.current = openCodeWindow; @@ -4619,6 +4618,7 @@ export default function App() { onSelect={setSelectedId} onHoverNode={setHoverNodeId} onOpenComponent={(name, id) => openComponent(name, pathFor(id))} + onOpenCode={openCodeWindowById} onChangeLayout={changeLayout} onDropComponent={addComponent} onMoveNode={moveNode} diff --git a/src/codeWindowTarget.ts b/src/codeWindowTarget.ts new file mode 100644 index 00000000..5317c476 --- /dev/null +++ b/src/codeWindowTarget.ts @@ -0,0 +1,38 @@ +import { assert } from '../shared/assert'; +import type { CodeWindowState, EditorNode } from './appTypes'; + +/** The page frontmatter: selectable like a node, but it is not one. */ +export interface FrontmatterSubject { + readonly id: 'frontmatter'; + readonly kind: 'frontmatter'; +} + +export type CodeSubject = EditorNode | FrontmatterSubject; + +export const FRONTMATTER_SUBJECT: FrontmatterSubject = { id: 'frontmatter', kind: 'frontmatter' }; + +// What the floating code editor opens for a subject. Enter, the "Edit code" +// button and a navigator double-click all ask this one rule, so they cannot +// disagree on the title or the language. Markup has no editor of its own: the +// answer is `undefined`, and Enter is left unhandled. +export function codeWindowFor(subject: CodeSubject): CodeWindowState | undefined { + if (subject.kind === 'frontmatter') { + return { targetId: 'frontmatter', title: 'Frontmatter', language: 'javascript' }; + } + if (subject.kind === 'raw') { + assert(subject.id.length > 0, 'raw node has an id'); + // The parser matches the tag case-insensitively but keeps it as written, + // so `