[diff/editor] Suppport custom clipboard provider#865
Open
ije wants to merge 52 commits into
Open
Conversation
* Remove history coalesce
* Fix selction/crate not updated when do "redo" command
* Remove visualColumns.ts
* Move editor ts files
* Refactor textarea buffer
* Rename `EditSnippet` type to `TextareaSnapshot`
* Remove `Editor` component, introduce the `Editor` class for `File` component
* Update demo
* Update editor constants to set text and background color to transparent
* Rewrite rerender logic
* Format
* Remove dead code
* Fix caret postion on empty line
* Improve `renderSelectionRange` performance by using cached DOM elements
* Support range selection in textarea
* Improve rerender performance
* Use piece table data sturcture for the text document
* refactor
* Add public `setSelection` method for the `Editor` class
* Add `FileContentsWithLineOffsets` interface and update related components to support line offsets and line count. Refactor file handling to utilize computed line offsets for rendering and iteration.
* Add `updateRenderCacheAt` method to `FileRenderer` and `File` classes for improved rendering. Refactor theme handling in `Editor` to utilize a dedicated method for color map retrieval.
* Refactor file iteration logic by removing `iterateOverFile` utility and replacing it with direct loops in `VirtualizedFile` and `FileRenderer` components. Update line offset computation to exclude trailing newlines in multi-line files while maintaining correct line counts. Enhance tests to validate line counting behavior.
* Remove EOF field
* Remove text length fields from HistoryEntry and related test cases in EditHistory
* Rename class `EditHistory` to `EditStack`
* Refactor EditStack and PieceTable to use a unified text slice interface.
* Refactor PieceTable and TextDocument to improve line offset handling and remove unnecessary EOL trimming logic.
* Refactor `Editor` to utilize new dirty line resolution logic, enhancing performance and accuracy in line tracking.
* Fix multi-cursor textarea sync
* Refactor Editor rendering logic for improved performance and reduce direct DOM manipulation.
* Add grammer cache
* Enhance line position caching in Editor for improved performance and accuracy.
* Refactor indentation handling in Editor and remove unused utility function for improved clarity and performance.
* Fix testing types
* Improve performance of the `getCharacterX` method
* Improve caching mechanism for enhanced performance.
* Add maxEntries feature to EditStack for managing undo history size
* Refactor
* Enhance PieceTable and TextDocument to trim line endings in getLineText method, improving text handling consistency. Update related tests for accuracy.
* Refactor
* Add `BackgroundTokenzier` class
* Improve performance
* Fix hightlight bug
* Add `--diffs-bg-caret` css property
* Fix input
* Fix selection range rendering
* Fix prebuildStateStackCache funciton
* Update `TOKENIZE_MAX_LINE_LENGTH` to 10,000
* Add `DiffsEditor` interface
* Fix `lineAnnotations` argument on `triggerEdit` invoke
* Refactor editor edit method to accept onChange callback directly and update demo to log file changes
* Clean up
* typo
* Refactor BackgroundTokenizer to use message-based scheduling.
* Refactor editor focus handling by removing redundant event listeners and updating CSS selectors for caret visibility.
* Refactor
* Fix `toTextareaSelectionDirection` function
* Refactor
* Update `DiffsEditor` types
* Add line annotation handling
* Add documentation for `hasVisibleLineAnnotation` function.
* Get rid of enum
* Clean up
* Refactor
* Update editor CSS
* Support text wrap
* Clean up
* Fix line y/wrap cache
* Fix line cache
* Copies leading indentation onto the new line after Enter
* Focus textare after undo/redo
* Move multi-selection functions to editorSelection module
* Add support for handling leading indentation deletion in applyTextChangeToSelections
* Fix selection glitch bug
* Add extendSelection command
* Fix `focusTextare` function
* Fix `resolveTextareaChange` function
* Remove unnecessary target check in mouseup event listener in Editor class
* Fix textarea selction direction
* Fix selection bg color for safair
* Clean up
* Fix shift select
* Refactor
* Refactor
* Fix shift select delay
* Coalesce edit stack entries for simple typing or backspace operations.
* Add Support forward-delete coalescing for edit history
* docs: add docs for editStack module
* Refctor
* Fix 'documentStart' and 'documentEnd' commands
* Rewrite selection handle logic
* Fix shouldCoalesceEditStackEntry function
* Update demo
* Add `removeEditor` for File component
* Add react api
* Clean up
* Update demo app
* Refactor useFileInstance to remove redundant editor cleanup logic
* Fix `computeLineOffsets` function
* typo
* Update editor style
* Fix `getOrCreateLineOffSets` method
* Refactor line count and annotation handling in File component; remove hasVisibleLineAnnotation utility
* Fix lines deletion crocss virtul viewport
* Remove `normalizeSelectionsForDocument` function
* Fix `edit` function
* Add editor sub-module
* Use `contenteditable` model
* Fix line wrap
* Fix wrap line
* Fix selection on mobile
* Update editor style
* Fix resize handling
* Add editor overlay layer
* Cleanup
* Add `DiffsEditableComponent` types
* Fix `VirtualizedFile` component
* Update `DiffsEditableComponent` type
* Add editor demo
* Fix slection rendering
* Update editor demo app
* Fix VirualizedFile component
* Update editor demo app
* Fix some selection bugs
* Update demo app
* Refactor findNextNonOverlappingSubstring method into PieceTable and TextDocument
* Refactor
* feat: Implement line jump
* Fix selection rendering when scrolling
* Improve tokenzier performance
* feat: simple search pannel
* Update editor demo app
* Fix jump
* Update search UI
* Add lag radar
* Fix virtualizer
* Fix render range after typing
* Fix editor tokenzier cache
* Fix search input focus
* Update log rader position
* Improve piece table performance
* Refactor
* Add lag radar
* Fix line count for empty documents
* Fix offscreen lines flush
* Introduce gutter width tracking
* refactor
* Refactor
* fix import
* Add 'expandSelectionDocStart' and 'expandSelectionDocEnd' commands
* Fix buffer height
* Add matches text for the search pancel
* Disable preious/next icon when no matches
* Update style.css
* feat: Support `quiteEdit` action
* Update edtior demo app
* Refactor
* Update demo app
* Update demo app
* Fix girdRow when render quick edit UI
* Move testing files
* Clean up
* Add searchPanel.ts
* Fix expandCollapsedSelectionToWord to match when the cursor is immediately touching one of the word's boundaries
* clean up
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Fix typo
* Clean up searchPanel and quickEdit when swith file
* Rebase to beta-1.2
* Fix selection after clean up quick edit widget
* Fix virtual buffer
* Fix `updateWindowSelection` method of Editor class
* Fix render range when typing new line at the end of the file
* Fix buffer when adding large lines
* [editor] Support 'deleteHardLineForward' input
* Add `insertTranspose` input
* Move `change` handler to options
* Update css
* Merge beta-1.2 changes
* Fix emply line rendering
* Add search settings UI
* Merge branch 'main' into editor
* Support FileDiff component
* Update `DiffsEditableComponent` interface
* Fix `getSelectionAnchor` function
* Fix text measurement for emoji
* Increase delay for diff rendering in FileDiff component
* Update types
* Add unit testings for text measue functions
* Clean up dirty render cache
* Fix `lineAnnotations` re-rendering
* Disable gutter utility when editing
* Add global css
* Fix scrollToLine method
* Refactor selection handling in Editor class to initialize selections properly and streamline rendering logic
* Fix diffs components
* Allow to create selection from gutter interaction
* Fix focus
* Fix browser compatibility
* Support dual themes
* Fix selection bugs
* refactor
* Add `Metrics` class
* Clean up
* Fix wrap selection rendering on safari
* Add `QuickEditContext` types
* Fix caret scroll margin when search panel is on
* Refactor search panel widget
* Fix selection position
* Update react components
* Update search panel CSS
* Fix quick edit
* Add editor docs
* Fix react hooks for editor
* Update editor demo component
* Update Quick Edit docs
* Update `diffStyle` and `expandUnchanged` options when editing
* demo: remove editor route
* Update docs
* Update docs
* Update examples
* Reset selection when 'Esc' key pressed
* Fix selection focus
* Add 'enable edit' shortcut('e')
* Handle the arrow key events to scroll to the cursor position manually
* Merge of overlapping selections
* Handle cursor moving events
* Fix scroll margin top
* Add debug logging option to Editor class
* Fix selection bugs
* Fix selection renering for unified `FileDiff`
* Reset ignore selection change flag on mouse up event
* Clean up
* fix bun.lock
* Add editor theme style
* Refactor
* Fix react types
* Fix last line index calculation
* Update condition for marking DOM dirty in VirtualizedFile component
* Throw if someone is trying to edit with no editor instance
* Update `mergeFileDiffOptions` function
* `lineOffsets` -> `lines`
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…d for the `DiffsEditor` (#766) * feat(editor): add pause and resume functionality for background tokenization * pref(editor): Introduce `postponeBackgroundTokenizeToNextFrame` method for the `DiffsEditor` * fix * Add debug option for the tokenzier * Update types * Refactor * typo * Reduce requestAnimationFrame calls
* chore: empty commit for beta branch * Homepage FileDiff editor demo * Style kbd elements, add beta badge to docs content, redo table for keyboards, few edits * docs(editor): add MultiFileDiff React example Document editing with MultiFileDiff alongside File and FileDiff in the React integration tabs. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(editor): use parseDiffFromFile in FileDiff React example Align the editor FileDiff tab with the pre-parsed fileDiff prop API. Co-authored-by: Cursor <cursoragent@cursor.com> * Update editor react examples * docs(editor): document worker pool usage with useTokenTransformer Add a Worker Pool section with tabbed vanilla/React examples, and enable useTokenTransformer on the docs site worker pool so editing works off-thread. Co-authored-by: Cursor <cursoragent@cursor.com> * format * Remove toolbar, put reset into header metadata * add link * little copy editing * Update homepage example to include file and diff * redo reset --------- Co-authored-by: Amadeus Demarzi <amadeusdemarzi@gmail.com> Co-authored-by: Je Xia <i@jex.me> Co-authored-by: Cursor <cursoragent@cursor.com>
* Rounded selection boundaries * Search panel refactor/redesign * Introduce `postponeBackgroundTokenizeToNextFrame` method for the `DiffsEditor`
…`deleteWordBackward` (#789)
Removing BETA badge from Virtualization
* [diffs/editor] refactor editor API * Refactor * refactor * fix * Refactor * refactor * refactor * Add blur method to Editor class for improved focus management * Update docs * Refactor
* Includes editor code refactor * Find/Replace functionality
fix(diffs): Handle IME composition input Fixes the following issue: 1. Open an editable diffs editor and place the caret in content. 2. Start a CJK IME or accented/dead-key composition. 3. Type a preview candidate, then commit it or press Esc to cancel. Previously, each preview beforeinput was prevented and warned as an unknown input type, so the browser could not show native preview text and canceled composition text could still be inserted on compositionend. Let insertCompositionText beforeinput events stay with the browser. Track composition updates and only commit non-canceled compositionend data into the editor model. Add regression coverage for preview, commit, warning noise, and canceled composition text. Also guard the selectionchange handler against browsers and embedded WebViews that lack Selection.getComposedRanges, which previously threw out of the listener on every selection change and left the editable surface unusable.
* [diffs/editor] Support editing unified diffs and refresh diff view on edit Allow the editor to edit unified diffs by syncing the render view against the FileDiff metadata (additions) instead of a separate addition file, and mark deletion/annotation lines non-editable in unified mode. Replace the post-edit `updateLineType` path with `refreshDiffView`/`fastRefreshDiffView` so the diff view re-renders correctly after edits. * Fix home live editor reset when toggling mode Include `mode` in the editable surface memo deps so switching modes recreates the surface instead of reusing a stale instance. --------- Co-authored-by: Mark Otto <markdotto@gmail.com>
* New homepage editor Co-authored-by: Cursor <cursoragent@cursor.com> * revamp * feat(diffs/editor): color marker hover popover by severity Tag the marker hover popover with its severity so the popover renders as a solid chip filled in the severity color with white text, mirroring the underline. Drops the inset surface ring on severity popovers so the fill reads as one solid color. * fix(diffs/editor): pin dual-theme surface tokenizer to themeType Only observe the document/system color scheme when the surface follows it (themeType: 'system'). A surface pinned to an explicit dark/light theme must re-tokenize after an edit with the same theme the SSR markup used; otherwise edited tokens fall back to the default foreground. * chore(demo): adapt marker message to severity-filled popover The marker hover popover is now filled with the severity color, so let the icon and description inherit the popover's white text instead of painting their own color (which would vanish against the fill). * refactor(docs): drive shortcut tables from ShortcutKeys Extract the platform-modifier logic into usePlatformModifier and add a ShortcutKeys component that renders a shortcut from plain string arrays, so a serializable shortcuts table can drive rendering without inlining JSX per row. Move the tree a11y shortcut table onto this model. * style(docs): default AUI editor demo to split diff Switch the homepage editor demo from unified to split diff style. * feat(docs): add Edit feature page Add a dedicated /edit feature page showcasing edit mode: live editing, selection actions, lint markers, find-in-file, undo history, and keyboard shortcuts, plus a reference section. Wire it into the header, mobile menu, and footer navigation, and link to it from the homepage agent demo. Rename the LiveEditor toggle from file/diff to a read-only Review vs editable Edit of the same File surface. * Redesign selection action icon a smidge * improve edit hero and content * vibe update docs * copy and layout updates * fixes mb * Fix AUI demo and more * fucking sick * better * better again --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Render marker popup in overlay element
The contenteditable host advertises role="textbox" but had no accessible name, so screen readers announced an unlabeled text field. Label it with the current file name in __syncRenderView. The content element is reused across file switches, so the label is refreshed on every sync rather than only when the element is first initialized.
A touch or pen tap on a line number ran the gutter selection logic, setting #isGutterMouseDown and attaching a document mousemove listener. The global pointerup that clears that state bails for non-mouse pointers, so the flag stayed stuck true and the listener leaked, with a fresh one added on every later gutter tap. The flag also feeds isMouseDown(), which the marker renderer reads. Add the same pointer-type guard the content pointerdown already uses so non-mouse taps bail early. Route both select-listener reassignment sites (the gutter drag tracker and the Safari annotation-hover workaround) through a #replaceSelectEventListeners helper that disposes the prior batch before assigning, keeping the dispose-before-replace invariant in one place so the sites cannot drift.
Editing a diff was re-diffing the whole file twice for every keystroke that adds or removes a line (Enter, Backspace that merges lines, typing over a multi-line selection). For such edits the editor calls updateRenderCache and then applyDocumentChange in the same pass, and both rebuild hunk metadata with a full recompute. The recompute in updateRenderCache runs against a half-updated line array and is immediately discarded by applyDocumentChange, which recomputes from the authoritative document text — so it is pure wasted work. It cannot be made incremental: updateDiffHunks falls back to a full recompute whenever the addition and deletion line counts differ, which is exactly a line-count change. Since applyDocumentChange already does the authoritative recompute, the editor now tells updateRenderCache to skip the hunk recompute on these edits. The per-line token and text updates still run, so the rendered output is unchanged.
editor.applyEdits applied edits to the buffer but called #applyChange with selections=undefined, so the selection-update path was skipped and #selections was never remapped against the edit. After a programmatic edit before the caret, getState().selections, the native window selection, and the on-screen caret all pointed at stale offsets, and the next keystroke was computed from the wrong position. Capture the selection edges and edit ranges as pre-edit offsets, remap each selection edge through the applied edits (an edge inside a replaced range collapses to the end of the replacement; direction is preserved), and pass the remapped selections to #applyChange. When updateHistory is set, record them as the entry's after-selections so redo restores the same caret. This matches every other edit path, which already remaps selections. Behavior change: a programmatic applyEdits now repositions the caret and, like undo/redo and other edits, focuses the editor and scrolls the caret into view. Hosts that previously relied on applyEdits leaving focus untouched will now see the editor take focus. Add test/editorApplyEdits.test.ts, which drives the public API through the jsdom harness: caret shifts down for an insert above it, both edges of a range shift while direction is preserved, the caret holds for an edit after it, and redo restores the remapped caret under updateHistory. Verified with moonx diffs:test (552 pass), diffs:typecheck, and root:format root:lint.
remapOffsetThroughEdits treated an offset equal to an edit's start as sitting before the edit, so inserting text at a collapsed caret left the caret on the left of the new text and the next keystroke landed in front of it. Use right gravity at the edit-start boundary so an offset at or after an edit's start follows the replacement, matching how typing advances the caret past inserted characters. Add a regression test covering an insert at the collapsed caret. Verified with moonx diffs:test (553 pass), diffs:typecheck, and root:format root:lint.
The editor caret animated with `blinking 1.2s infinite` regardless of the OS reduced-motion preference. Add a `prefers-reduced-motion: reduce` media query that drops the animation so the caret renders as a solid bar instead of pulsing.
Route cut events through a dedicated cut path so caret-only cuts copy and remove the full logical line instead of behaving like copy. With multiple selections, every collapsed caret cuts its own line while ranged selections cut their text, and overlapping cuts are merged so shared text is removed and copied only once. Unify copy and cut clipboard text behind a shared getSelectionText helper so the two stay in sync: a collapsed selection now contributes its whole logical line including the trailing line break (the final line has none), and separators synthesized between non-contiguous regions use the document's detected line ending. Note this also changes a plain copy of a caret to include the line's trailing newline. Add editor clipboard regression tests (single, multi-cursor, mixed, same-line, and range-overlapping-caret cuts, plus copy) and expose the jsdom globals needed to mount the editor in those tests.
Shift-select goal column. Put the caret well into a long line (say column 20), then hold Shift and press Down across a shorter line onto another long line. The selection should keep extending at column 20, but the focus snapped to the end of the short line and stayed there on later moves. mapSelectionShift round-tripped the focus through a document offset, which clamps the column to the short line's length; it now carries the focus position directly so the preferred column survives. Auto-surrounding or running a selection action over such an overshooting selection also pulled in the short line's trailing newline, so getText now clamps positions to the visible line before slicing. Multi-caret Tab indent. With several cursors on one line (say columns 0 and 5 of "abcdefgh"), press Tab. The indent was re-applied once per cursor, over-indenting the line, and the second cursor landed before its own indent (column 7 instead of 9), so the next keystroke went to the wrong place. Tab now batches the per-caret edits into one change and shifts each later same-line caret past the indents inserted ahead of it. Linux Ctrl+A / Ctrl+F. On Linux these triggered Emacs cursor moves (line start / forward char) instead of select-all and find; those bindings are now limited to macOS. Supporting fixes: truncate cached file rows when an edit removes lines (stale rows lingered after deletions); keep the default tab size when the computed tab-size is non-numeric; and reset cached platform detection on each test DOM install so platform-specific tests stay isolated.
The editor is commonly embedded next to other inputs — for example an AI chat box that writes generated code into the editor while the user keeps typing in the chat box. A host inserts that generated code with editor.applyEdits(). Before this change, the programmatic edit pulled focus into the editor and scrolled to the caret, interrupting whatever the user was typing elsewhere on the page. Repro: 1. Render a File with the editor plus another text input on the page. 2. Click into the editor to place a caret, then click into the other input and start typing. 3. While focus is in the other input, run editor.applyEdits([...]) (an AI action, a codemod, or a call from the devtools console). Before: Focus jumps to the editor and the view scrolls to the caret, cutting off the user's typing. After: focus stays in the other input and the editor updates quietly, with its caret left in the correct, remapped spot for when the user returns. Cause and fix: applyEdits routes through `#applyChange`, which always called focus() and scrolled the caret into view. It now tracks the contenteditable's focus via focus/blur listeners (`#contentHasFocus`) and passes skipFocus to `#applyChange` when the editor is not focused at edit time. The selection is still re-anchored either way, so the caret is correct once the user returns; only the focus and scroll side effects are suppressed. Already-focused editors (typing, undo/redo, in-editor edits) are unchanged. One timing case: `#focus()` defers the real focus() call to a requestAnimationFrame, so in a same-tick setSelections()/focus() then applyEdits() flow the focus event has not fired yet. That edit would wrongly be treated as unfocused and skip repositioning, while the queued focus landed afterward on a stale native selection. `#focus()` now sets `#contentHasFocus` eagerly so a same-tick edit still repositions. Tests: applyEdits leaves an unfocused editor's focus untouched, an already-focused editor still repositions, and a same-tick setSelections/focus + applyEdits flow repositions.
A programmatic applyEdits on an unfocused editor re-anchors #selections but, with skipFocus, never syncs the native Selection. The global selectionchange handler did not check focus, so a DOM-driven or refocus selectionchange whose range still belonged to the editor could overwrite the remapped caret with a stale position before the user returned. Bail out of the handler when the contenteditable is unfocused. Pointer and programmatic focus set the focus flag first, so those paths are unaffected.
After a skipFocus applyEdits, #selections is remapped but the native DOM Selection is left at its old range. On a keyboard or direct programmatic refocus the focus listener sets #contentHasFocus before selectionchange fires, so the unfocused guard no longer suppresses the stale range; it overwrites the remapped caret — putting the next keystroke at the pre-edit position. Re-assert the primary selection onto the native Selection when the editor regains focus without a pointer gesture. A pointer focus is left to the click, and #focus() already syncs during an editor-driven focus.
Line-by-line search compiled a new RegExp from the pattern's source and flags on every line, so a search over an N-line document built N identical regexes for no benefit. Reuse the single compiled pattern and reset its lastIndex before each line instead. Add a regression test for matches within and across lines, which had no happy-path coverage before.
Type a character, paste some text, then press undo. Both the paste and the typed character disappear together, but undo should remove only the paste. Paste and cut go through the same path as typing, and the undo stack only looks at the shape of an edit. A single-line paste looks just like more typing, so it merged into the previous keystroke's undo entry. (Multi-line pastes were kept separate only because they change the line count.) Mark paste and cut as undo boundaries so each stays its own step and never merges with the edit before or after it. Typing still merges as before. Also stop the undo check from merging a newline insert into earlier typing, so the shared helper is safe for any caller.
Steps to reproduce: 1. Open the editor on a page that uses a custom monospace web font. 2. Load it with a cold cache so the editor renders before the font file finishes downloading. 3. Click in the code to place a caret. The caret and selection sit left of the real text and the gutter is too narrow. The editor measures the '0' character width on first render and uses it to size the gutter and place every caret. Before the web font loads, that width comes from the fallback font. getComputedStyle returns the same font-family string before and after the font arrives, so init() never re-measures and the wrong width sticks. Re-measure the '0' width once document.fonts.ready resolves. If it changed, drop the cached widths and offsets and repaint the carets, selections, and markers so they match the loaded glyphs. Do nothing when the width is unchanged or the font was already loaded. The re-measure is scheduled once per mount; cleanUp() clears the guard so a reused editor instance schedules it again for a font that may not be loaded yet.
`#computeChangedLineRange` counted only `\n` when sizing the inserted line span, but the piece table (and `lineCount`/`lineDelta`) count `\n`, `\r`, and `\r\n` via `computeLineOffsets`. Inserting lone-`\r` (classic Mac) text therefore under-scoped `changedLineRanges` and `endLine`, leaving the tokenizer to skip re-highlighting new lines. Add a `countLineBreaks` helper that applies the same single-pass, CR/CRLF-aware scan as `computeLineOffsets` without allocating an offsets array, and use it for the inserted line span. Cover the fix with lone-`\r` change-range tests plus a unit test asserting `countLineBreaks` stays in lockstep with `computeLineOffsets`.
Put a tab after other text on a line, like `foo<tab>bar`, then place the caret right after the tab. The caret and selection box landed a column past the real glyph. With line wrapping on, selecting tabbed text that starts partway into a visual row was mis-sized too. The editor renders tabs with CSS tab-size, so a tab advances to the next tab stop (the next multiple of the tab size). The measurement code instead added a full tab size for every tab, which is too wide for any tab that follows other characters. Leading indentation still lined up because each tab there already starts on a tab stop. Advance each tab to the next tab stop from its running column, both in the ASCII column count and in the tab-to-space expansion used for canvas and DOM measurement. For wrapped selections, measure the width as the gap between two offsets taken from the segment start (which sits on a tab stop) instead of measuring the sliced selection on its own, matching how the non-wrapped path already works. Add regression tests for mid-line tabs and the slice-versus-offset difference.
Steps to reproduce: 1. In the diffs editor, edit a line with an emoji, e.g. "a😀". 2. Put the caret after "a" and press Right. The caret lands inside the emoji instead of moving past it; a later insert or delete then splits it into a lone surrogate. 3. Put the caret at the end of "a😀" and press Ctrl-T. Instead of swapping to "😀a", the emoji is split into mojibake. Cause: mapCursorMove stepped left/right by one UTF-16 code unit and transpose swapped single code units. The document is UTF-16, so an emoji takes two units; both operations landed on or split a surrogate pair, even though the module already segments graphemes for word operations. Fix: move horizontally by whole grapheme clusters and transpose whole graphemes, via a shared getLineGraphemeStarts helper built on Intl.Segmenter (also reused by deleteWordBackward). ASCII behavior is unchanged. Add regression tests for left/right move, shift-select, and the mid-line, end-of-line, and cross-line transpose branches.
The editor could only undo and redo through keyboard shortcuts. Apps that drive the editor (toolbar buttons, AI edits, custom shortcuts) had no way to trigger or check undo state. Expose `undo()`, `redo()`, `canUndo`, and `canRedo` on the Editor class. The methods reuse the existing command path, so programmatic and keyboard undo behave the same and consumers still get the `onChange` callback after each one.
* fix(diffs): memoize and round DOM text measurements Steps to reproduce: 1. Open the diffs editor on a line containing an emoji, such as const greeting = "😀 hello". 2. Move the caret along that line or extend a selection over it. 3. Watch the Performance panel: every caret or selection update forces a synchronous layout. For each non-ASCII run the editor inserts a hidden span, reads getBoundingClientRect(), and removes it, with no caching, so the same runs are re-measured on every render. That path also returns the raw sub-pixel width while the canvas and ASCII paths round, so offsets are quantized inconsistently across runs on the same line. Fix: - Memoize domMeasureTextWidth() by measured text, capped at 4096 entries with FIFO eviction, so repeats skip the reflow. - Clear the cache in init() when the font changes, since cached widths are font-specific. - Round the DOM-measured width to match canvasMeasureTextWidth and the ch metric. Add regression tests for the rounding, one measurement per distinct run, and cache invalidation on font change. * fix(diffs): clear text width cache on layout reflow The DOM text-width cache was only cleared on a font change in init(), which runs only for a new content element. When a web font finished loading or font-related styles changed on the same content element, cached emoji/non-ASCII widths could stay stuck at the previous font. Clear the cache from the editor's resize handler, where the sibling DOM-measurement caches (wrap offsets, line Y, last caret X) are already invalidated on a width change, so the widths re-measure after such a reflow. * Fix text wdith cache key --------- Co-authored-by: Je Xia <i@jex.me>
* some tweaks * add editing to playground * MAYBE: fix theme detection * MAYBE: fix wrapping breaking markers and cursor when in split mode * Update editor theme css * Fix split diff with wrap * Fix selection flash * fix menus on playground for mobile * update liveediting demo for better mobile --------- Co-authored-by: Je Xia <i@jex.me>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a configurable clipboard provider to the diffs Editor so consumers (especially Electron apps) can bypass the browser paste/clipboardData path that may stall the renderer.
Changes:
- Introduces
EditorOptions.clipboardwith areadText()hook. - Intercepts Ctrl/Cmd+V to trigger a custom paste flow.
- Updates docs type snippets and demo usage to showcase the new option.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/diffs/src/editor/editor.ts | Adds the clipboard option and a custom Ctrl/Cmd+V paste path. |
| apps/docs/app/(diffs)/docs/Editor/constants.ts | Documents the new clipboard option in the Editor options type snippet. |
| apps/demo/src/main.ts | Demonstrates passing a clipboard provider to the Editor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The browser's default
pasteevent listener can freezing the renderer process. This pr addedclipboardoption for the Editor allows you to handle thepasteevent. Highly recommended to use native clipboard API if you are building an electron app.