Skip to content

[editor] Beta 1.3#749

Draft
amadeus wants to merge 87 commits into
mainfrom
beta-1.3
Draft

[editor] Beta 1.3#749
amadeus wants to merge 87 commits into
mainfrom
beta-1.3

Conversation

@amadeus

@amadeus amadeus commented May 29, 2026

Copy link
Copy Markdown
Member

Used to track editor beta versions. Will be periodically rebased on main.

@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Jun 27, 2026 4:36pm
pierre-docs-diffshub Ready Ready Preview Jun 27, 2026 4:36pm
pierre-docs-trees Ready Ready Preview Jun 27, 2026 4:36pm
pierrejs-docs Ready Ready Preview Jun 27, 2026 4:36pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pierrejs-diff-demo Skipped Skipped Jun 27, 2026 4:36pm

Request Review

@amadeus amadeus mentioned this pull request May 29, 2026
15 tasks
necolas and others added 30 commits June 24, 2026 16:22
Repro (diffs /playground, edit mode):
1. Select all text in the editor.
2. Press delete/backspace.

Before this fix the editor breaks: split mode collapses to a
single uneditable view and undo does nothing; unified mode keeps
the view but you can no longer type.

The editor's text document always keeps one (empty) line, but
splitFileContents('') returns [], so emptying the editable side
recomputed the diff with zero addition lines. The additions
column then rendered no line elements, leaving the attached
editor with nothing to host its caret.

Now an emptied document is represented as one empty editable
line: diff the unchanged deletions against a single empty line
and store the addition as [''] so it still joins back to the
editor's empty document. Covered by model- and DOM-level
regression tests in both split and unified modes.
When the editor is emptied, the recompute diffs the deletions
against a single empty line to place one editable row. If the
old side was itself a single blank line, that diff was a no-op
(zero hunks), so iterateOverDiff emitted nothing and the row was
still missing.

Pick a sentinel that always differs from the deletion side so a
hunk is produced; its text is discarded by the [''] override.
Covered by the empty-document regression tests.
Turn on word wrap in an editable diff and type in a line until it
grows long enough to wrap onto another row. Now click a line below
it, or select text there: the caret and the selection highlight land
a row too high, sitting on the wrapped line's extra row instead of
the line you clicked.

The editor caches each line's vertical position and only refreshed it
when the number of lines changed. Wrapping a line adds a visual row
without changing the line count, so the cached positions of the lines
below it stayed stale, and the caret and selection drawn from them
rendered a row too high. Refresh the cached positions after an edit
whenever wrap is on, matching how the wrap offsets are already
invalidated.

This corrects only the vertical position of overlays on the lines
below a wrap. Selecting a word on the wrapped line itself still
mispaints the highlight horizontally; that is a separate bug.
Steps to reproduce:
1. Scroll a long file so the visible lines sit inside an unclosed
   block comment or template literal.
2. Delete the line directly above the viewport.
3. The visible lines lose their comment color and render as if they
   were plain code.

The tokenizer read the loop's grammar state before the offscreen
flush rebuilt the cached state stack up to the viewport's first line,
so it captured INITIAL. The visible lines were then tokenized as if
outside the construct — corrected by a later background pass, or never
when the viewport reaches the end of the document and no background
pass is scheduled.

Seed the loop state after the offscreen flush instead. This reuses the
state the flush already computed, so it adds no tokenization work and
only changes the delete-reaches-viewport case; far-above edits with a
gap stay seeded from INITIAL and are corrected by the background pass
as before.
Paste inserted clipboard text verbatim, so a Windows clipboard
(CRLF or CR) left mixed line endings in a file that otherwise uses
one style, showing up as spurious diff noise. Rewrite clipboard line
breaks to the document's detected EOL before inserting, matching what
copy already does.

Expose the line ending as TextDocument.eol with a normalizeEol()
helper and move the generic endsWithLineBreak() predicate into editor
utils, so this logic no longer sits among the selection helpers.
Repro:
1. Open the editor on a large file, or make many scattered edits so the
   document accumulates many internal fragments.
2. Keep typing; each keystroke gets progressively slower.

Every edit (typing, deleting, applyEdits) rebuilt the editor's whole
text structure from scratch, so a single edit cost time proportional to
how fragmented the document had become; in a heavily edited file each
keystroke paid for the entire document.

That structure (a piece table) keeps text as a list of "pieces" indexed
by a tree. The tree is now a treap: a binary search tree that also
keeps each node's random priority in heap order, so it stays balanced
without an explicit rebalancing pass. An edit now splits the tree at
the edit, drops the removed part, and merges the new text in, touching
one root-to-leaf path (O(log P)) instead of rebuilding all P pieces.
Reads and editor behavior are unchanged.
Steps to reproduce:
1. Scroll a long file so the editor virtualizes (only a window of
   lines is rendered).
2. With a caret near the window bottom, insert lines there - press
   Enter on the last rendered line, or paste a few lines.
3. The just-typed line, and its caret, are missing from the window
   until the next scroll.

#applyChange widened the render range only when the caret was exactly
at the window's end, and never persisted the widened range, so a
following edit read a stale renderRangeEndLine, treated the caret as
past the window, and #rerender (clamped to the range) never built the
new row; #isLineVisible/#renderCaret read the stale range and dropped
the caret too. (Only edits that carry a caret reach this path - it is
guarded on `selections` - so a bare programmatic applyEdits with no
active selection is unaffected; the real triggers are typing and paste
at the window bottom.)

Widen the range to cover the caret line for inserts that reach the
window's bottom edge and persist it to #renderRange so consecutive
edits stay accurate and the caret draws.

Bound that widening two ways so it can't defeat virtualization or
render a gap. Cap it at twice the bounded window the virtualizer last
synced (captured in __syncRenderView as #viewportWindowLines): a large
insert at the caret - most often a big multi-line paste - can drop the
caret far below the window, and widening to reach it would build a row
per inserted line synchronously, risking a freeze. And skip widening
when the edit starts below the window, since #rerender only builds rows
from change.startLine - widening there would leave the intervening rows
unbuilt while reporting them visible, mispositioning the new rows. Past
either limit, keep the bounded window and only recompute the buffer,
leaving the far region for the scroll that follows a focused edit (or
the next user scroll) to render. Capturing the window at sync time also
stops consecutive edits from ratcheting the cap up.

Add editorVirtualizedEdit.test.ts: consecutive newlines and a small
multi-line insert at the window bottom render; a 1000-line insert, a
run of consecutive inserts, and an edit starting below the window all
keep the rendered window bounded and contiguous.
Open a split diff with word wrap on and edit a line so it wraps onto
several rows. Double-click a word on a wrapped row: the word is
selected and the caret lands correctly, but the highlight is drawn at
the row's left edge instead of over the word. Selecting whole lines
across a multi-line range shows the same shift.

In a split diff the editable panel sits to the right of the deletion
panel. The caret math adds this horizontal panel offset, but the
selection-highlight math did not, so every highlight was pulled left
by the offset (its width stayed correct). Add the same content offset
when computing the highlight's left edge, for both wrapped rows and
selections that start at the beginning of a line.
In a split diff with word wrap on, edit a line so it wraps, then turn
word wrap off. Select a whole line from its start: the highlight either
vanishes or jumps far to the right of the text.

#contentOffset caches the split panel's horizontal shift but is only
set in a split + wrap diff and never cleared, so toggling wrap off
leaves a stale value on the same editor. The selection start added that
stale offset while the end (from #getCharX) did not, giving a negative
width. Read the offset through a getter that returns it only while the
live layout still applies, so caret, selection, and line-Y math all
ignore a stale value.
* Phase 1: Setup the new slot architecture

* Phase 2: Vanilla API shenanery

* Phase 3: React implementation

* Phase 4: Add CodeView support

* Add demo-ability

* Phase 5: Adding misc tests

* Phase 6: Adding docs

Both for the new API and also some forgotten docs for header prefix
[diffs/editor] Updated `editor.css` for marker popups to improve layout and responsiveness
Steps to reproduce:
1. Open a diff in edit mode and edit a line (e.g. rename a symbol).
2. Toggle any display option - word wrap, theme, diff style, line
   numbers.
3. The edit vanishes and the line shows its original text again; it
   only reappears on the next keystroke.

When an editor is attached, its document is the source of truth for the
content, but the host passes a static fileDiff. A display-option change
forces a full re-render that rebuilds the diff rows from that fileDiff,
so the in-progress edits are painted over with the original content -
and inserted or deleted lines are lost.

After a full re-render, when the editor's document survived it and the
rendered rows no longer match it, re-render the diff from the document:
rerenderFromDocument re-derives the diff and clears the render cache so
the rebuild re-highlights from the edited contents. One pass restores
text, syntax colors, and line count, with no per-row reconciliation.
The diff is mutated in place so its cacheKey is kept and the editor's
document and undo history survive.

Gated to full re-renders (a scroll's partial render reuses the existing
rows) and to components with a document-backed re-render (FileDiff; the
plain File has no such path yet). Host-agnostic: this holds whether or
not the host sets a cacheKey. Adds a regression test covering the
toggle, further typing, an inserted line via both render APIs, and
downstream block-comment highlighting - all with no cacheKey.

Co-authored-by: Je Xia <i@jex.me>
* chore: empty commit for beta branch

* Prevent transparent borders

* Fix the editing example

* fix cursor jumping in history demo

* wip

* updates

* button fixes, history improvement

* refactor(diffs): show the selection action in a floating popover

Selecting text in an editable surface used to surface a lightning icon
in the gutter; clicking it inserted the consumer's action element as a
new inline row, which reflowed the document and took two interactions.

Replace that with a floating popover that appears automatically once a
ranged selection settles, anchored just below the selection's head and
mounted in the overlay layer so it never reflows the content. The
`renderSelectionAction` API is unchanged, and its handlers now read the
live primary selection so keyboard-extending a selection keeps acting on
the current range. Drop the now-unused gutter icon, its `quick` sprite,
and the inline-row styling.

* docs(docs): rebuild the selection action demo as "Add to chat"

The edit page's selection demo wrapped the selection in t() or shouted
it in caps from a toolbar. Rebuild it around the new popover: selecting
code pops an indigo "Add to chat" action (plus a secondary copy) that
sends the snippet to a mock chat panel beside the editor, complete with
an inert composer mirroring the homepage agent UI.

Snippets render with a read-only File. The page's shared worker pool is
wired for the editable surface and doesn't highlight a dynamically
mounted read-only File, so opt these out with disableWorkerPool and
highlight on the main thread. The panel is height-matched to the editor
so the list scrolls while the header and composer stay put.

* docs(docs): describe the selection action popover

Update the editor docs and copy to match the new behavior: the action
appears in a floating popover anchored to the selection instead of a
gutter icon you click, and it can hold any number of actions. Refresh
the edit-page blurb, the Editor guide prose, and the option comment, and
drop the selection action from the standalone EditorDemo so it stays
focused on plain editing.

* chore(demo): match the renamed selection action popover hook

The selection action now renders as a popover, so target the new
[data-selection-action-popover] selector and drop the inline margin that
spaced the old inline action row.

* test(diffs): cover selection action popover lifecycle

Add cases for the popover's auto show/teardown: it disappears once the
selection collapses, and it never renders (nor calls the consumer's
callback) when enabledSelectionAction is left off.

* feature updates

* New replace and replaceall icons

* better search shortcuts

* Update keyboard shorcuts table

* refactor search, fix some nits, redo icons in search, combine marker severity styles

* remove labels

* use claude to restore the delta between X's and my changes, then reapply to markers and popovers

* improve focus, rearrange

* localize the padding/margin for now

---------

Co-authored-by: Amadeus Demarzi <amadeusdemarzi@gmail.com>
* Revamp some AUI demo code, streamline a few things, update selection action

* streamline the aui

* fix playground theme flash when toggling review/edit

* Track live diff stats in AgentUi Changes tree

Recompute each file's added/removed line counts from in-editor edits and
update the +/- decorations in the Changes tree so they reflect the live
diff rather than the static snapshot counts.

* fix(docs): render heading anchor inline with FeatureHeader titles

Drop the flex layout on the FeatureHeader h2 so the injected "#" anchor
flows inline after the title text instead of floating beside wrapped
multi-line headings.

* fix hint coloring on light mode, plus fix playground theme flash
more mobile related bugs
In edit mode, click or drag to select code on either side of a split or
unified diff. A caret highlights only its own line, on the side it sits
on, instead of also lighting up the paired line in the read-only
deletions pane. Selecting text drops the full-line background and keeps
only the caret line's number highlighted, so the selection itself is the
line-level marker. Deleted text is selectable and copyable: clicking a
line number selects that whole line's text on either side, and in split
view dragging across the deletion line numbers selects the block, the
same as on the additions side.

Previously the active-line highlight leaked onto the read-only deletions
pane, the full-line background competed with a text selection, deleted
text could not be selected, and line-number gestures behaved differently
per side. The editor now confines the active-line highlight to the
caret's side, paints deleted-text selection with the native selection
(revealed only while selecting deleted text), highlights just the focus
line's number during a gutter drag, and stops a transparent-span rule
from hiding the word-level diff highlights.
fix(diffs): clear bufferBefore and bufferAfter on reset
The diff editor's "system" themeType follows the OS prefers-color-scheme
(its shadow root declares `color-scheme: light dark`), so it drifted from
the app whenever the app theme differed from the OS. Resolve "system" to
the app's scheme from @pierre/theming so the editor stays in sync; the
switcher stays editor-only and "light"/"dark" remain independent overrides.
Rename the useTheme() binding and every consumer to the controller's
vocabulary so the docs match the library, not just one file:

  resolvedTheme -> resolvedColorScheme
  theme         -> colorMode
  setTheme      -> setColorMode
  systemTheme   -> systemColorScheme
  themes        -> colorModes

Also rename the playground's local switcher state (themeType -> colorMode,
the diffs `themeType` option key is unchanged) and the pre-paint bootstrap
script's locals. Pure rename — no behavior change.
Editing a scrolled unified diff snaps the view back to the top. To
reproduce:

1. Open the editor on a unified diff.
2. Scroll down within the editor.
3. Edit a visible line (type a character, press Enter, etc.).

The view jumps to the top of the diff instead of staying where you
were. Split diffs are unaffected.

On every edit a unified diff re-renders its rows in place:
FileDiff.refreshDiffView rewrites the content column's innerHTML,
which detaches the line elements the editor cached for caret
geometry. The editor then measures a detached row (offsetTop 0) and
places the caret at the top, so the follow-up scrollIntoView pulls
the viewport up to it.

Reset the editor's line-geometry caches after a diff rebuilds its
rows, so the caret re-measures against the fresh DOM and the
viewport stays put.
On the homepage editor demo, scroll a changed file down, then open a
different file from the Changes list: it opens scrolled to the previous
file's offset instead of at the top.

The demo reuses one FileDiff surface across files (never remounted, so
the prerendered markup survives), and the scroll lives on the host
`.aui-surface-wrap` container, which nothing reset on a file switch.
Reset its scrollTop to 0 whenever the active file changes, in a layout
effect so the new diff never paints at the old offset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants