Skip to content

🐛 fix: a code editor keeps its Tab in a dialog, and undo after a pasted carriage return restores the text - #733

Merged
edgarmesquita merged 15 commits into
mainfrom
fix/an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr
Oct 10, 2026
Merged

edgarmesquita merged 15 commits into
mainfrom
fix/an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr

Conversation

@edgarmesquita

@edgarmesquita edgarmesquita commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #598
Closes #600

What

A code editor behaves the same inside a dialog and after any paste, with no line of an app changed.

Neither the public surface nor the developer surface moves.

Why

  • A code editor at either end of a dialog loses Tab to the dialog's focus trap #598: the dialog's focus trap listened for Tab on the window in the capture phase, before the editor's input, and cycled the focus at the dialog's ends. It now hears the key on its way back up and cycles only a Tab nothing consumed. That is how Flutter's focus tree answers it (a key goes to the primary focus first, and focus traversal runs only for a key nothing below it handled), and the question Photon's host already asks its code target.
  • Undo and redo after pasting a lone carriage return restore the wrong text #600: the document breaks lines on CR, CRLF and LF alike, and an edit recorded the text as it was handed, which the history split on LF alone. Pasting "x\ry" into "abc" after its first character gave "ax" and "ybc", and undo left "a" and "ybc". An edit now carries the text the document holds, and CodeEdit.InsertedRange breaks lines as the document does, whoever builds the edit.

Reported on its own: a spreadsheet takes every Tab and releases none on either target, so the keyboard cannot leave one inside a dialog either (#732).

OpenSpec

an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr, archived. It creates code-history and modal-focus.

Proof

  • CodeClipboardTests: undo and redo after pasting a lone CR, a CRLF, a LF and a mix, the edit a listener hears, and the range of an edit built by hand. On main's engine the five with a lone CR fail, and CRLF and LF pass there too, as the issue says.
  • ATypedLoneCarriageReturn_EndsTheRunOfTyping, from Copilot's first round: the history's run of typing ended at LF alone, so a typed lone CR a host recorded ran on into the next character, and one undo took back both. It fails on the history before the fix.
  • focus-trap.spec.ts: a Tab the focused control consumed is not cycled. code-editor.spec.ts: the real code surface, lowered and mounted at either end of a marked layer, indents on Tab, outdents on Shift+Tab, and moves on after Escape. On main's trap all four fail, the last one because the editor indented after the trap had already moved the focus.
  • The engine's twins regenerated (CodeEdit.ts, CodeEditorController.ts, CodeHistory.ts) with their pins (14), the runtime's tsc and vitest 2253, Native.Engine 1925 (main e5ad1df merged), the Web suite's docs and wiki guards (15), and samples/DefaultUIDashboard built.

Checklist

  • dotnet test on the affected test projects, and dotnet build src/eQuantic.UI.Runtime -t:TestRuntime
  • This PR's checks include a run named CI with jobs in it
  • ./scripts/check-openspec.sh passes, and the change is archived before the merge
  • dotnet build samples/DefaultUIDashboard
  • A broken contract has a line in the migration notes: none breaks
  • The documentation changed with the behaviour: the wiki's CodeEditor page, in English and Portuguese, on the branch named like this one
  • One docs/LEDGER.md line, citing both issues
  • The diff reviewed by its author before the PR opened (/code-review high, six findings: the spreadsheet that keeps every Tab reported as A spreadsheet never gives Tab back, so the keyboard cannot leave it #732 and fenced in the design, the rest stated)
  • No "widget" in prose

…nd redo replays it

The document breaks lines on CR, CRLF and LF alike, and an edit recorded the text as it was handed, which the history split on LF alone: pasting "x\ry" into "abc" after its first character gave "ax" and "ybc", and undo left "a" and "ybc". An edit now carries the text the document holds, and its range breaks lines where the document does, whoever made it. Closes #600.
The dialog's focus trap heard Tab in the capture phase, before the editor's input: with the editor last, Tab moved the focus to the first control and the line was never indented, and Shift+Tab did the same with the editor first. The trap now hears the key on its way back up and cycles only a Tab nothing consumed, the question Photon's host asks its code target before it moves the focus.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

CodeHistory.Record ainda permite que uma edição digitada com CR seja agrupada com o próximo caractere.

1 open finding
What changed in this PR

Corrige o comportamento de Tab em editores dentro de diálogos e restaura corretamente o histórico após colagens com CR/CRLF.

Changes:

  • Faz o focus trap respeitar controles que consumiram Tab.
  • Normaliza quebras de linha registradas no histórico.
  • Adiciona testes, especificações OpenSpec e registro no ledger.
File Description
tests/​eQuantic.UI.Native.Engine.Tests/​CodeClipboardTests.cs Testa colagem, undo, redo e normalização.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeEditorController.ts Registra o texto normalizado.
src/​eQuantic.UI.Runtime/​src/​shared/​components/​CodeEdit.ts Calcula intervalos para CR, CRLF e LF.
src/​eQuantic.UI.Runtime/​src/​shared/​code-editor.spec.ts Testa Tab nas extremidades do diálogo.
src/​eQuantic.UI.Runtime/​src/​dom/​focus-trap.ts Move o tratamento de Tab para bubbling.
src/​eQuantic.UI.Runtime/​src/​dom/​focus-trap.spec.ts Verifica Tab consumido pelo controle.
src/​eQuantic.UI.Code/​Editing/​CodeEditorController.cs Normaliza o texto registrado nos edits.
src/​eQuantic.UI.Code/​Document/​CodeEdit.cs Torna intervalos e inserções sensíveis a CR.
openspec/​specs/​modal-focus/​spec.md Especifica o compartilhamento de Tab.
openspec/​specs/​code-history/​spec.md Especifica normalização, undo e redo.
openspec/​changes/​archive/​2026-10-08-an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr/​tasks.md Registra as tarefas concluídas.
openspec/​changes/​archive/​2026-10-08-an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr/​specs/​modal-focus/​spec.md Arquiva o delta de foco modal.
openspec/​changes/​archive/​2026-10-08-an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr/​specs/​code-history/​spec.md Arquiva o delta de histórico.
openspec/​changes/​archive/​2026-10-08-an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr/​proposal.md Documenta escopo e impacto.
openspec/​changes/​archive/​2026-10-08-an-editor-keeps-tab-in-a-dialog-and-undoes-a-pasted-cr/​design.md Documenta as decisões técnicas.
docs/​LEDGER.md Registra as correções de #598 e #600.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/eQuantic.UI.Code/Document/CodeEdit.cs Outdated
…feed does

Copilot's first round on #733. The history asked whether an edit broke a line by LF alone, so a typed lone CR, which a host recording its own edits may hand it, ran on into the character typed after it, and one undo took back both. The edit now says whether it breaks a line by the document's rule, and the history and IsSimpleInsert ask it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

As correções são consistentes entre engine e runtime e possuem cobertura direta dos casos regressivos.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

…ditor family

The ledger line moves to the end, and the engine's twins regenerate unchanged.
CodeEdit's twin conflicted where both sides had regenerated it, and is regenerated again from the merged C#. The ledger line moves to the end.
The code editor's spec conflicted where both sides added tests at its end: both are kept, and the dialog's tests now clean the page after each one, since the completion list's tests from #653 run after them and a layer left marked pulled their focus into itself. The ledger line moves to the end, and the twins regenerate unchanged.
No conflict. The ledger line moves to the end.
No conflict. The ledger line moves to the end, and the twins' pins hold.
No conflict. The ledger line moves to the end.
No conflict. The ledger line moves to the end, and the twins regenerate unchanged.
@edgarmesquita
edgarmesquita merged commit ccdefbf into main Oct 10, 2026
9 checks passed
edgarmesquita pushed a commit that referenced this pull request Oct 10, 2026
Merging main left this pull request's entry above #733's, which landed
before it. The list is in the order things land, so it moves to the end,
unchanged.
edgarmesquita added a commit that referenced this pull request Oct 10, 2026
…lows .NET's chunks (#748)

Closes #679

It is a sub-issue of #565 (the transpiler's fences hold on every path).

## What

Every `StringBuilder` member a page reaches answers as .NET's does in
the browser, or fails the build where it cannot cross:

- **`AppendFormat` and `AppendJoin`** append what `string.Format` and
`string.Join` write, through the same lowering, bound by the same
parameters and under the same culture policy.
- **`Capacity` is .NET's.** The runtime keeps the sizes of the chunks
.NET allocates, so `Capacity` follows every edit: an append fills the
last chunk and opens one as large as the text so far (up to 8,000) or as
what is left, an insert or a longer replace makes room as `MakeRoom`
does (in place in a small chunk with room, otherwise a chunk of at least
16, one per source chunk for a replace), a removal unlinks the chunks it
empties, and a shorter `Length` keeps `min(Capacity, max(Length * 6 / 5,
the last chunk))`. `MaxCapacity`, `EnsureCapacity`, the `Capacity`
setter and the constructors that take a capacity, a maximum or a range
answer and refuse as .NET's do, in .NET's order. Text that fits the last
chunk goes in past `MaxCapacity`, as on .NET.
- **The `Chars` indexer** is the twin's `item` and `setItem`, refused
past the text as .NET refuses it. **`Length`** cuts the text or fills it
with `\0`. **`Equals(StringBuilder)`** compares the text whatever the
capacity, while `Equals(object)` stays identity. **`CopyTo`** copies
into a `char[]`.
- **What cannot cross is refused at the build**: a provider other than
the current culture on `Append` and `AppendLine` with an interpolation
(EQ2108), `GetChunks` and an overload over a span or a memory (EQ1004).
A `params` span is the values written one by one, and crosses.
- **A member that .NET makes of several appends makes them as .NET
does.** An interpolated `Append($"…")` or `AppendLine($"…")` appends
each part in turn, as .NET's interpolation handler does, so a hole that
reads the builder sees the parts before it (`$"{b.Length}{b.Length}"`
held "00" where .NET holds "01"), each part grows the chunks, a refusal
keeps the parts before it, and a hole prints as a plain interpolation's
does, where an enum reached the builder as its camelCase key.
`AppendLine(value)` appends the newline after the value,
`Append(StringBuilder)` checks the maximum before it copies, and a
`Replace` past the maximum keeps the chunks it replaced first.
- **A named argument is bound to its parameter**, each still evaluated
in the order written: the twin takes C#'s parameters by position.
- Every text the builder makes is built before its chunks change, and
one longer than the browser's string can hold is .NET's
`OutOfMemoryException`, not JavaScript's `RangeError`.

For an app: `sb.AppendFormat("{0}-{1}", 1, 2)`, `sb[0]`, `sb.Capacity`
and `sb.Length = 4` work in the browser, where they were TypeErrors,
undefined or writes nobody read, and `sb.Append($"{sb.Length}…")` writes
what .NET writes. Nothing an app writes changes, and the public surface
does not move.

## Why

The BCL audit graded every `StringBuilder` member `native`, which only
said that a strategy answered. The runtime's builder had `append`,
`appendLine`, `insert`, `remove`, `replace`, `clear`, `toString` and a
`length` getter, and eqc wrote every other member under its own name, so
a page reached TypeErrors and undefined values behind a green build
(#679, found while fixing #650 in #667).

## OpenSpec


`openspec/changes/archive/2026-10-07-a-builder-has-the-members-a-page-reaches`,
archived into `openspec/specs/transpiler-bcl`.

## Proof

- `StringBuilderConformanceTests`, both sides executed: 31 of the first
88 new cases failed on the base, the rest pinning members that answered
by accident. `Capacity` matches .NET 10 on 41 measured sequences.
- The review (`/code-review high`) found nine things, each measured on
.NET 10 with `dotnet fsi` before it was fixed: the room an insert or a
replace opens (`Capacity` 16 on .NET where the browser said 6), the
ranged constructor's refusals, the constructor's order of refusals,
`CopyTo`'s order, the refusal of an insert past `MaxCapacity`
(`requiredLength`, and `OutOfMemoryException` for a counted insert),
text that fits the last chunk past `MaxCapacity`, `OutOfMemoryException`
in place of a `RangeError`, and emptied chunks left in the list. 11 of
its 12 new conformance cases fail on the commit before.
- The BCL audit's verdicts move as meant (`AppendFormat` and
`AppendJoin` are `eq`, `GetChunks` is fenced), and EQ2108 gains
`StringBuilderStrategy` as a reporting site with its documented meaning,
which `docs/DIAGNOSTICS.md` now names.
- The served runtime: main records 213,928 bytes gzipped and serves
214,116 after #733. This branch measures 216,219, past the record's 1%,
so it moves the record to 216,148 (bf400ef, measured on the main of
#690, which 216,219 stays inside), in a commit that says what the bytes
bought.
- Suites, each alone and read by its exit code. On the main of #689:
Compiler (1,824), Conformance (4,555 of 4,556, one skipped) and Web
(988, the wiki guards on this pull request's wiki branch). On the main
of #733, this head: the runtime's `TestRuntime` (2,262) and Server (318,
the budget included), with Compiler, Conformance and Web rerunning.
- Copilot's second round found two defects, fixed in 229d3c8, and
measuring them on .NET 10 found four more in the same model, fixed with
them. A named argument reached the twin in the order written
(`CopyTo(count: 2, destination: a, …)` read the count as the source
index): a call that names an argument goes through `ParameterTemplate`,
each still evaluated in the order written. Only `String.repeat` mapped a
`RangeError`: every text the builder makes goes through one guard, built
before the chunks change. In V8 (Node 24), append, insert and replace
past the engine's longest string threw `RangeError: Invalid string
length` on the commit before and throw `OutOfMemoryException` now, the
builder unchanged. The four more: an interpolated append part by part,
`AppendLine`'s newline as an append of its own,
`Append(StringBuilder)`'s check of the maximum, and a `Replace` that
refuses chunk by chunk. On the commit before, 14 of the 16 new
conformance cases, 10 of the 12 new compiler cases and the 3 new runtime
cases fail, the runtime's under V8 and JavaScriptCore alike. The cases
that pass there pin what must not change: `Append(object)` holding a
builder is a string's append, a builder appended to itself is its text,
and a string's append stays one append. `Append(T)` of a float, a double
or an enum still prints what the browser holds, filed as #750.
- Copilot's first round found two defects, fixed in e7a65cc: the span
fence let a span passed whole to a `params` overload through (only the
values a call expands into it cross now, EQ1004 otherwise), and a
capacity past `Array.MaxLength` was accepted where .NET throws
`OutOfMemoryException` (measured for the constructors, `Capacity` and
`EnsureCapacity`). The one new compiler case and the three new
conformance cases fail on the commit before.

## Checklist

- [x] `dotnet test` on the affected test projects, and `dotnet build
src/eQuantic.UI.Runtime -t:TestRuntime` if TypeScript changed (the
embedded Bun runs `tsc`, then `vitest run`)
- [ ] This PR's checks include a run named CI with jobs in it
(`build-packages`, `test (ubuntu-latest)`, `test (windows-latest)`,
`openspec`, `session-start`, `wiki-checkout`). A workflow whose
expression does not parse creates zero jobs, and the ruleset's required
checks then keep the PR blocked. GitHub lists the workflow by its file
path instead of `CI`. While GitHub Actions has no credits, the same run
is on eQuantic Space (`eqs runs ls`)
- [x] `./scripts/check-openspec.sh` passes: the OpenSpec change
validates strictly, and it is archived before the merge
- [ ] `dotnet build samples/DefaultUIDashboard` (and `PhotonDesktop` /
`WalletMobile` if the native track changed) (CI's `samples` job builds
them)
- [x] A broken contract has a line in the migration notes (we are in
preview: break freely, hide nothing): no C# an app writes changes. A
builder member that was a TypeError now answers, and `GetChunks`, a span
overload and a provider other than the current culture fail the build
where they failed in the browser.
- [x] The documentation changed with the behaviour: this repository's
Markdown here (`docs/DIAGNOSTICS.md`, the ledger), and the wiki page in
English AND Portuguese, in one commit on a branch of the wiki repository
named exactly like this pull request's branch (`SupportedFeatures.md`)
- [x] One `docs/LEDGER.md` line for this event, citing the issue
- [x] The diff reviewed by its author before the PR opened (in Claude
Code, `/code-review high`); every Copilot thread answered and resolved;
a new round asked for only after a defect, and three rounds at most
- [x] No "widget" in prose; the project's word is *component*

---------

Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
edgarmesquita pushed a commit that referenced this pull request Oct 10, 2026
…ain took first

Merging main left this pull request's entry above #733's, #748's and
#731's, which landed before it. The list is in the order things land,
so it moves to the end, unchanged.
edgarmesquita pushed a commit that referenced this pull request Oct 11, 2026
Merges 20066eb, the third of the five steps that bring this branch up to main eb33f44. It
carries #666, #672, #688, #653, #699, #689, #690, #696, #733 and #748, and #748 is the one that
meets this branch's source.

How each source conflict was resolved:

- ValueFlow.cs: both sides taught FlowsIntoText the hole of an interpolated string a handler takes
  (sb.Append($"...")), this branch by the hole's syntax (an AppendFormatted with no alignment and no
  format clause) and #748 by the bound operation (an argument of the handler's call that is the
  call's only argument written). #748's rule is kept, since it reads the operation the hole is,
  and the hole's text is still this branch's: a value that flows into text goes through
  StringConversion, which writes it in the page's culture.
- InterpolatedStringStrategy.cs: #748 moved a hole's lowering into Hole(), which the builder's part
  by part append calls too, and this branch had changed the number kind the hole passes. Main's
  Hole() is kept with this branch's kind in it: FormatKind.Of where a specifier is written, and
  FormatKind.OfText with none, every number but a double.
- StringBuilderStrategy.cs: #748 rewrote the dispatch (the span and GetChunks fences, named
  arguments bound through ParameterTemplate, AppendFormat, AppendJoin, the interpolated append part
  by part, appendBuilder and equalsBuilder), and this branch had written the value of Append,
  AppendLine and Insert in the culture through its own bound call. Main's dispatch is kept, and the
  value goes in front of it through the same ParameterTemplate.Call, its convert writing the value
  argument through StringConversion and every other argument as it is, each evaluated in the order
  it is written. A value that is text already passes as it is, as before (ValueInTheCulture, IsText).
  BoundArguments.Call, which this branch added for that value alone, goes: nothing else calls it.
- docs/DIAGNOSTICS.md: EQ2108 keeps this branch's Convert.ToString and gains #748's builder clause,
  EQ2109 stays retired, and EQ2110 keeps this branch's reading-only meaning.
- diagnostics.baseline.txt: EQ2108 gains #748's StringBuilderStrategy, EQ2109 stays out, and EQ2110
  stays this branch's, ParseCulture alone.
- bcl-surface.baseline.txt takes main's side and is regenerated after the last step.

The runtime's twins in conflict take main's side, and every twin is regenerated after the last
step. docs/LEDGER.md merged as a union, and this branch's line moves back to the end of the events.
@edgarmesquita edgarmesquita mentioned this pull request Oct 11, 2026
5 of 6 tasks
edgarmesquita added a commit that referenced this pull request Oct 11, 2026
Closes #756

The bump to `0.2.0-preview.61`, cut from main at 9305100 (#637) at
Edgar's request. It carries forty-five pull requests since `.60`: a
component that hears the server on a typed topic (#647), a page that
requires its authorization and a Server Action that refuses another
site's request (#686, #737), a layout that follows its box and type that
follows the window (#648, #665), a control's press and focus on every
target with a 24dp floor under a pointer (#617, #628), the code editor's
completion (#618, #653, #733), a hot reload under `dotnet watch` that
keeps the page's state (#666, #672), the image optimizer on SkiaSharp
(#730), the places where the producers drew one tree differently, with
Photon's hit-testing and its text layout (#688, #690, #699, #739, #741,
#749), a number and a date printed in the page's culture (#637), and a
long family of browser answers made .NET's (#579, #588, #594, #607,
#608, #621, #654, #656, #660, #661, #667, #668, #689, #696, #697, #705,
#707, #708, #709, #715, #731, #748, #752), with a page that answers HEAD
(#610) and the conformance harness's own comparison (#609).

## What this commit does

- `Directory.Build.props`: `0.2.0-preview.60` → `0.2.0-preview.61`.
- `./scripts/public-api.sh ship`: nine projects, 462 declarations join
the shipped surface and 43 retire, and every `Unshipped` is empty
afterwards. Checked after the fold, per project and compared as sets:
the lines that left `Shipped` are exactly the 43 `*REMOVED*` lines
declared on main, and the lines that joined are exactly its 462
declarations, so an orphan could not vanish unnoticed. The line diff
reads +463 −549: the version line, 462 lines into `Shipped` and 43 out
of it, and the 505 that leave the `Unshipped` files.
- The analyzer accepts the fold: the solution builds with no RS0016 or
RS0017. And it read the files: one bogus declaration appended to an
`Unshipped` failed the build with RS0017.
- Against `v0.2.0-preview.60` the `Shipped` diff has those 43 lines gone
and no other, so no pull request rewrote a shipped line in place this
time, as #512 did in `.60`.

## What the retirements are

Read out of the `*REMOVED*` lines, each paired with the line that joined
under the same member and attributed by the commit that wrote it, not
from the log. Only `ServerActionHub` left with nothing in its place, and
the rest changed shape:

- `ServerActionHub` and its constructor (#647): the hub, its route
`/_equantic/hub` and the `AddSignalR()` call `AddUI` made are gone. An
app that injected `IHubContext<ServerActionHub>` publishes through
`IServerEventPublisher`, and one that maps hubs of its own calls
`AddSignalR()` itself.
- `ICodeCompletionProvider.CompleteAsync` (#618) takes a
`CodeCompletionContext` and answers a `CodeCompletionList`.
`CurlyBraceLanguage.Keywords` was the protected abstract set a language
overrode, and it is the public list a completion offers now.
- `Drawing`'s constructor, `Drawing.Width` and `UI.Drawing` (#648) hold
the width as a `SizeValue`. `new Drawing(art, 240)` converts, and a read
into a `float` becomes `drawing.Width.Value`.
- `ImageOptimizer()` and `BlurPlaceholderGenerator()` (#730) take the
`ImageOptimizationOptions`. `UseImageOptimization` registers both, so
only an app that built one with `new` meets it.
- `HitRegion` (#690) gains `Drawn` after `Bounds`, so building one and
deconstructing one both change. `CodeRegion` (#653), `SheetRegion` and
`TextRegion` (#690) gain optional members at the end, so only a
positional deconstruction changes.
- `ServerActionsMiddleware`'s constructor (#737) takes an
`IOptionsMonitor<ServerActionsOptions>`. `UseServerActions()` builds it,
so only an app that constructs the middleware itself meets it.
- Four signatures widened at the end by optional parameters, which
compiles as before from source: `UI.Positioned` by the four fractions
and the two shifts (#648), `UI.Pressable` by `canRequestFocus` (#653),
`WebRealizer.Lower` by `density` (#688) and `PhotonRealizer.Realize` by
`sizedLike` (#690).
- `eQuantic.UI.Compiler`, which no app references, 21 lines: eleven of
#661 (`JsTemplate`'s `Annotate`, with its constructor, its `Deconstruct`
and the factory that took it, and six LINQ strategies that crossed to
the IR and lost their text `Convert`), four of #608 (`ValueMember`,
`ComponentDependencyResolver`'s constructor and
`RecordTypeEmitter.CanEmit`), two of #637 (`Eq.AsInteger` and
`Eq.AsSingle`), two of #654 (`UserDefinedOperators.Binary` and `Unary`),
and `HashSetStrategy` (#594) and `RangeIndexerStrategy` (#707) crossing
to the IR.

One break is an addition, which no `*REMOVED*` line shows.
`CurlyBraceLanguage.ReservedWords` (#618) is a new abstract member, so a
language derived from it renames its `Keywords` override to
`ReservedWords`. I listed every member that joined an interface or an
abstract class already shipped, and it is the only one an implementer
has to write: `ICodeLanguage.Keywords` and
`ICodeCompletionProvider.ResolveAsync` have default implementations.

The additions are 118 in `eQuantic.UI.Code` (the completion), 116 in
`Primitives` (the server events vocabulary, `FluidSize`, `Positioned`'s
fractions and shifts, `BoxStyle.Pressed`, `GridTrack.AutoFill`,
`Touch.MinPointerTarget`), 98 in `Server` (server events, the origins a
Server Action allows), 86 in the compiler, 19 in `Native.Components`, 12
in `Web`, 7 in `Components`, 4 in `Native.Framework` and 2 in `Images`.

## The rest of the surface

`developer-surface.baseline.txt` gains two configuration sections since
`v0.2.0-preview.60`, `EQuantic:ServerActions` (#737) and
`EQuantic:ServerEvents` (#647), and loses nothing: no MSBuild property
or template parameter came or went.

What an app meets with no signature moving:

- Three new build errors. EQ1009 (#608, #621, #708) stops two
constructors that take the same number of arguments, in a class, a
record or a struct that reaches the browser, where the twin kept one and
a `new` that meant another ran it. EQ2014 (#708) stops an exception
built or tested where a type argument it carries is a type parameter.
EQ2013 (#647) stops a `ServerTopic<T>` built over a type parameter,
which only new code can meet.
- Refusals widened under existing codes: EQ1007 by a second indexer and
a sequence over two types (#707, #708), EQ2004 by a stored `Range`, a
method group over a .NET extension and a .NET collection's own
`GetEnumerator()` (#656, #707, #708), EQ2007 by a comparer handed to
`ToDictionary`, `ToLookup`, `GroupBy` or `Distinct` (#707), and EQ2010
by an extension method of a server-only class and a type nested in one
(#654, #705, #731).
- Two refusals a running app meets on the server: a Server Action called
from another origin answers 403 until the app lists that origin (#737),
and a class-level `[Authorize]` guards the page as well as its actions
(#686).
- `image/avif` in the optimizer's `Formats` stops the app at startup
(#730), and a `Spacer` of weight below 1 or a negative `Flexible` weight
throws where it is written (#741).
- EQ2109 is retired and EQ2110 is about reading a number now, since a
number written as text follows the page's culture on both sides (#637).

The rest are browser answers made .NET's and drawings made alike across
the producers, listed in the tag.

## The wiki's version marks

`WikiVersionMarkTests` compares a mark only once a tag holds its symbol,
so the `.61` tag is when this release's marks are judged for the first
time. Emulated before the tag, with the test's own pattern and lookup
and a `.61` at 9305100: 145 marks, no mismatch. The thirteen it judges
first resolve to `.61`, as written: `StartFraction`,
`GridTrack.AutoFill`, `canRequestFocus`, `DENSITY_COOKIE`, `Drawing`'s
`SizeValue` width, `WithPageAuthorization`, `AllowServerActionOrigins`,
`IServerEvents`, `MapGetAndHead`, `UseServerEvents`, `TwinIsData` on the
tokens, `BoxStyle.Pressed` and `ScrolledThreshold`.

## The tag's gate, rehearsed

`gate-templates` runs only on a `v*` tag, so a template that does not
build costs a release attempt. Rehearsed on main at 9305100, before
this branch: 50 packages packed at `1.0.0-dev`, the three web and the
four native template shapes scaffolded and built in Release against that
feed alone, with no warning and with every eQuantic package restored at
the version packed, the published `runtime.js` identical byte for byte
to the Server package's, and the dashboard template served, hydrated and
navigated in a browser with no console error.

## Not in this release

No pull request is open. Forty-eight bugs are, none of them a regression
against `.60`. Three issues came out of this stretch and wait for a
decision or their turn: #753 (`string.Split` with an array of
separators, a count or a null separator), #754 (an app's exception class
in a snippet compiled on its own) and #755 (whether generic code should
carry its type arguments at run time, which EQ2014 fences).

The release notes travel in the annotated tag. The wiki's Upgrading page
carries the distilled entry in both languages, on the wiki branch
`chore/0.2.0-preview.61`, which merges into the wiki's master when this
merges and before the tag. The `docs/LEDGER.md` line for the release
follows the tag, as `.60`'s did, because it names the commit the tag
lands on.

## Checklist

- [x] The diff reviewed by its author before the PR opened: a version
line and a mechanical fold, verified as sets above
- [x] `dotnet test` on this branch: Compiler 1898, Server 374,
Conformance 5087 with one skipped, Web 1021, and the runtime's own 2355
in 170 files, none aborted
- [ ] This PR's checks include a run named CI with jobs in it
- [x] `./scripts/public-api.sh ship` run, every `Unshipped` empty, the
retirements read from the diff
- [x] The documentation changed with the release: the Upgrading entry in
English and Portuguese, on the wiki branch `chore/0.2.0-preview.61`,
with the wiki guards passing against it (9 of 9, and the vocabulary
guard fails on that page when a word it refuses is written into it)
- [x] Copilot's rounds answered: one round, approval recommended with no
finding and no thread, so the loop ends there

Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants