-
Notifications
You must be signed in to change notification settings - Fork 1
✨ Describe terminal grids as executable document structure (#729) #733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -74,7 +74,9 @@ import { | |||||
| strayCaseMessage, | ||||||
| strayElseMessage, | ||||||
| strayStructuralMessage, | ||||||
| strayTerminalMessage, | ||||||
| switchStructure, | ||||||
| terminalGridStructure, | ||||||
| } from "./structural-rules.ts"; | ||||||
| import type { StructuralViolation } from "./structural-rules.ts"; | ||||||
| import type { | ||||||
|
|
@@ -314,6 +316,8 @@ interface LexicalContext { | |||||
| readonly insideIf: boolean; | ||||||
| /** Whether a `<Switch>` in this source lexically encloses this point. */ | ||||||
| readonly insideSwitch: boolean; | ||||||
| /** Whether a `<Terminal.Grid>` in this source lexically encloses this point. */ | ||||||
| readonly insideTerminalGrid: boolean; | ||||||
| /** Whether the immediate parent is an `<Answers>`. */ | ||||||
| readonly underAnswers: boolean; | ||||||
| } | ||||||
|
|
@@ -492,6 +496,7 @@ class ValidationState { | |||||
| insideLoop: false, | ||||||
| insideIf: false, | ||||||
| insideSwitch: false, | ||||||
| insideTerminalGrid: false, | ||||||
| underAnswers: false, | ||||||
| }); | ||||||
| } | ||||||
|
|
@@ -1102,6 +1107,24 @@ class ValidationState { | |||||
| return context.insideSwitch | ||||||
| ? [] | ||||||
| : [{ code: "structural-usage-invalid", source: "Case", message: strayCaseMessage() }]; | ||||||
| case "Terminal.Grid": | ||||||
| // The whole layout is decided from source, so every pane's own mistake | ||||||
| // is reported where it was written — and so is a construct written | ||||||
| // below the grid that the grid does not lay out. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment — restates what the code does.
Suggested change
|
||||||
| return terminalGridStructure(segment).violations; | ||||||
| case "Terminal": | ||||||
| // A well-placed `<Terminal>` is its grid's, and one placed wrongly | ||||||
| // under a grid is already reported by that grid's own structure. What | ||||||
| // is left is a pane with no grid above it at all. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment — restates what the code does.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment — restates what the code does.
Suggested change
|
||||||
| return context.insideTerminalGrid | ||||||
| ? [] | ||||||
| : [ | ||||||
| { | ||||||
| code: "structural-usage-invalid", | ||||||
| source: "Terminal", | ||||||
| message: strayTerminalMessage(), | ||||||
| }, | ||||||
| ]; | ||||||
| case "Else": | ||||||
| // A well-placed `<Else>` is its `<If>`'s, and one placed wrongly under | ||||||
| // an `<If>` is already reported by that `<If>`'s own structure. What is | ||||||
|
|
@@ -1272,6 +1295,7 @@ function childContext(segment: ComponentElement, context: LexicalContext): Lexic | |||||
| insideLoop: context.insideLoop || segment.name === "Loop", | ||||||
| insideIf: context.insideIf || segment.name === "If", | ||||||
| insideSwitch: context.insideSwitch || segment.name === "Switch", | ||||||
| insideTerminalGrid: context.insideTerminalGrid || segment.name === "Terminal.Grid", | ||||||
| underAnswers: segment.name === "Answers", | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -13,7 +13,7 @@ | |||
| * middleware installation) execute before children's code blocks. | ||||
| */ | ||||
|
|
||||
| import { ensure, Err, scoped, useScope, withResolvers } from "effection"; | ||||
| import { ensure, Err, Ok, scoped, useScope, withResolvers } from "effection"; | ||||
| import type { Operation, Result } from "effection"; | ||||
| import type { | ||||
| FunctionComponent, | ||||
|
|
@@ -57,9 +57,17 @@ import { | |||
| strayCaseMessage, | ||||
| strayElseMessage, | ||||
| strayStructuralMessage, | ||||
| strayTerminalMessage, | ||||
| switchStructure, | ||||
| terminalColumns, | ||||
| terminalColumnsMissingMessage, | ||||
| terminalGridStructure, | ||||
| terminalTitle, | ||||
| terminalTitleMissingMessage, | ||||
| } from "./structural-rules.ts"; | ||||
| import type { StructuralViolation, SwitchCase } from "./structural-rules.ts"; | ||||
| import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts"; | ||||
| import { terminalGridLayout } from "./terminal-grid.ts"; | ||||
| import type { PlacedPane } from "./terminal-grid.ts"; | ||||
| import { | ||||
| asBindingViolation, | ||||
| asExpressionViolation, | ||||
|
|
@@ -1173,6 +1181,28 @@ function* expandListSegments( | |||
| break; | ||||
| } | ||||
|
|
||||
| if (segment.name === "Terminal.Grid") { | ||||
| // No raise() here, like the branches above: expandTerminalGrid | ||||
| // reports every error it creates. | ||||
| yield* expandTerminalGrid(segment, result); | ||||
| break; | ||||
| } | ||||
|
|
||||
| if (segment.name === "Terminal") { | ||||
| // A well-placed <Terminal> is consumed by its <Terminal.Grid> and | ||||
| // never expanded on its own. Reaching this branch means the pane sits | ||||
| // outside every grid, so it names no component and is diagnosed | ||||
| // rather than resolved from the filesystem. | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment — restates what the code does.
Suggested change
|
||||
| result.push( | ||||
| yield* raise({ | ||||
| type: "error", | ||||
| message: positioned(strayTerminalMessage(), segment), | ||||
| source: "Terminal", | ||||
| }), | ||||
| ); | ||||
| break; | ||||
| } | ||||
|
|
||||
| if (segment.name === "Break") { | ||||
| result.push(...(yield* expandBreak(segment, loop))); | ||||
| break; | ||||
|
|
@@ -2028,6 +2058,128 @@ function* expandSwitch( | |||
| ); | ||||
| } | ||||
|
|
||||
| function terminalGridError(segment: ComponentElement, message: string): ErrorSegment { | ||||
| return { type: "error", message: positioned(message, segment), source: "Terminal.Grid" }; | ||||
| } | ||||
|
|
||||
| function terminalPaneError(segment: ComponentElement, message: string): ErrorSegment { | ||||
| return { type: "error", message: positioned(message, segment), source: "Terminal" }; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * The value one prop of a terminal-grid construct produced, or why evaluating | ||||
| * it failed. A missing prop is `undefined`, which is also what an expression | ||||
| * evaluating to `undefined` leaves behind (§6.5) — absence either way, and the | ||||
| * caller says what its construct requires instead. | ||||
| */ | ||||
| function* resolveStructuralProp( | ||||
| segment: ComponentElement, | ||||
| construct: string, | ||||
| prop: string, | ||||
| ): Operation<Result<Json | undefined>> { | ||||
| const expression = segment.expressions[prop]; | ||||
| if (expression === undefined) { | ||||
| return Ok(segment.props[prop]); | ||||
| } | ||||
| try { | ||||
| const resolved = yield* resolveExpressionProps( | ||||
| {}, | ||||
| { [prop]: expression }, | ||||
| construct, | ||||
| segment.projectedEnv, | ||||
| ); | ||||
| return Ok(resolved[prop]); | ||||
| } catch (error) { | ||||
| return Err(error instanceof Error ? error : new Error(String(error))); | ||||
| } | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Open the grid the author wrote (spec §6.21). | ||||
| * | ||||
| * The whole layout is decided before anything opens: the panes and their forms | ||||
| * from source, then `columns` and each pane's `title` from the values the | ||||
| * document computes. Only once the concrete grid is complete is a terminal | ||||
| * provider anything's business — and this build has none, so the grid refuses | ||||
| * there. Nothing beneath a pane has expanded and no shell has started when it | ||||
| * does, which is what makes the refusal a closed one rather than a partial grid | ||||
| * left behind. | ||||
| */ | ||||
| function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> { | ||||
| const structure = terminalGridStructure(segment); | ||||
| if (structure.violations.length > 0) { | ||||
| for (const violation of structure.violations) { | ||||
| owner.push(yield* raise(structuralErrorSegment(violation, segment))); | ||||
| } | ||||
| return; | ||||
| } | ||||
|
|
||||
| const columnsValue = yield* resolveStructuralProp(segment, "Terminal.Grid", "columns"); | ||||
| if (!columnsValue.ok) { | ||||
| owner.push(yield* raise(terminalGridError(segment, columnsValue.error.message))); | ||||
| return; | ||||
| } | ||||
| if (columnsValue.value === undefined) { | ||||
| owner.push(yield* raise(terminalGridError(segment, terminalColumnsMissingMessage()))); | ||||
| return; | ||||
| } | ||||
| const columns = terminalColumns(columnsValue.value); | ||||
| if (!columns.ok) { | ||||
| owner.push(yield* raise(terminalGridError(segment, columns.error.message))); | ||||
| return; | ||||
| } | ||||
|
|
||||
| const placed: PlacedPane[] = []; | ||||
| for (const pane of structure.panes) { | ||||
| const title = yield* resolvePaneTitle(pane); | ||||
| if (!title.ok) { | ||||
| owner.push(yield* raise(terminalPaneError(pane.element, title.error.message))); | ||||
| return; | ||||
| } | ||||
| placed.push({ title: title.value, form: pane.form }); | ||||
| } | ||||
|
|
||||
| const layout = terminalGridLayout(columns.value, placed); | ||||
| owner.push( | ||||
| yield* raise({ | ||||
| type: "error", | ||||
| message: positioned(noTerminalProviderMessage(), segment), | ||||
| source: "Terminal.Grid", | ||||
| // The grid the author asked for, carried beside the sentence so an | ||||
| // assertion is about the layout that was derived rather than about the | ||||
| // wording of a refusal. | ||||
| cause: { | ||||
| layout: { | ||||
| columns: layout.columns, | ||||
| rows: layout.rows, | ||||
| cells: layout.cells.map((cell) => ({ ...cell })), | ||||
| }, | ||||
| }, | ||||
| }), | ||||
| ); | ||||
| } | ||||
|
|
||||
| /** The label one pane displays, from the value its own `title` prop produced. */ | ||||
| function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> { | ||||
| const value = yield* resolveStructuralProp(pane.element, "Terminal", "title"); | ||||
| if (!value.ok) { | ||||
| return value; | ||||
| } | ||||
| if (value.value === undefined) { | ||||
| return Err(new Error(terminalTitleMissingMessage())); | ||||
| } | ||||
| return terminalTitle(value.value); | ||||
| } | ||||
|
|
||||
| /** What a complete grid says on a host where nothing can open one. */ | ||||
| function noTerminalProviderMessage(): string { | ||||
| return ( | ||||
| "no terminal provider opened this grid. A host installs the terminal-grid capability " + | ||||
| "explicitly, and this one installs none, so no pane expanded its content and no default " + | ||||
| "shell started." | ||||
| ); | ||||
| } | ||||
|
|
||||
| function loopError(segment: ComponentElement, message: string): ErrorSegment { | ||||
| return { type: "error", message: positioned(message, segment), source: "Loop" }; | ||||
| } | ||||
|
|
||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant comment — restates what the code does.