Skip to content

feat(editor): first-class Cut/Copy/Paste — visible, snapped, guarded, and bend-safe - #270

Merged
byrongamatos merged 4 commits into
mainfrom
feat/editor-clipboard
Jul 15, 2026
Merged

feat(editor): first-class Cut/Copy/Paste — visible, snapped, guarded, and bend-safe#270
byrongamatos merged 4 commits into
mainfrom
feat/editor-clipboard

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Copy/paste existed — but only as hardwired inline keydown code: invisible in the Edit menu, the shortcut panel, and the command palette (this session's recurring failure mode), no Cut at all, and four latent defects. All three are first-class registry commands now.

  • copySelection / cutSelection / pasteAtPlayhead — Ctrl+C / Ctrl+X / Ctrl+V in the FeedBack profile; the EOF profile keeps its legacy Ctrl+X = mute-open, so Cut is Shift+Del there. Edit-menu rows added; the palette and shortcut panel pick them up from the registry automatically.
  • Cut = copy + the existing undoable delete; undoing a cut restores the notes but keeps the clipboard (the text-editor contract, pinned by test).
  • Paste lands at the snap-honouring playhead (was: raw cursor time), keeps the phrase's internal timing (relative-time packing), selects the pasted notes, and is one undoable step.

Latent bugs fixed on the way

  1. Shared bend curves: techniques were copied with a shallow spread, so the original and every paste shared one bend_values array — editing any of them silently edited all. Pack and plan now structuredClone; independence is pinned by test.
  2. Ghost-lane pastes: pasting a 6-string riff onto a 4-string bass wrote notes on strings the track doesn't have. Notes that don't fit are skipped and counted in the status ("2 skipped — no such string on this track").
  3. Keys ↔ fretted pastes produced nonsense (string/fret mean different things there) — refused with a plain-English status.
  4. Cross-track pastes into the read-only fretted roll are pitch writes — gated by the existing lock notice like every other write.

The old inline blocks are removed; dispatch now flows through the same resolver/registry path as every command (both hand resolvers gained the three bindings). Ctrl+D duplicate is untouched.

Tests

tests/clipboard.test.mjs (6, fail on main): relative-time packing, deep-clone independence (mutating the original, or one paste, never touches the others), the retime/clamp/lane-skip plan, the full copy→paste flow through the real EditHistory (one undo entry; exec → rollback → redo reproduces exactly), the cut/undo/clipboard-survives contract, and the mode guards. Full suite 147 green, lint 0 errors, routes.py untouched.

Live-verified

Real project, real keystrokes: Ctrl+C → "Copied 4 notes", seek to 100s, Ctrl+V → "Pasted 4 notes at the playhead" (1114 → 1118 notes, exactly one undo entry), Ctrl+X → "Cut 2 notes" (→ 1116). Zero page errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features
    • Added first-class Edit ▸ Copy / Cut / Paste commands and corresponding keyboard shortcuts (including legacy and native mappings).
    • Copy/Cut uses a session clipboard; Paste places pasted content on the grid at the playhead, preserves internal timing, keeps pasted notes selected, and supports a single undo step.
    • Paste skips items that don’t exist on the destination tracks and refuses keys↔fretted pasting; paste is blocked in drum and tempo-map modes and in read-only views.
  • Bug Fixes
    • Pasted bend curves are no longer linked to the originals.
  • Tests
    • Added clipboard and end-to-end cut/copy/paste undo/redo coverage.
  • Documentation
    • Updated the changelog with the new command behaviors.

Copy/paste existed only as inline keydown code — invisible to the menu,
shortcut panel and palette, no Cut, shallow technique copies sharing
bend-curve arrays across pastes, raw-cursor paste, and no lane/kind
guards (pasting onto fewer strings wrote notes on lanes that don't
exist). Now: registry commands copySelection/cutSelection/
pasteAtPlayhead (Ctrl+C/X/V; EOF keeps Ctrl+X=mute so Cut is Shift+Del
there), Edit menu rows, deep-cloned pack/plan pures with relative
times, snap-honouring paste at the playhead, lane clamp with honest
skip counts, keys<->fretted refusal, read-only-roll guard for
cross-track pitch writes, pasted notes selected, one undoable step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: addaba90-99e2-4c24-b536-638abc23ce45

📥 Commits

Reviewing files that changed from the base of the PR and between 2b46316 and 421eca7.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/input.js
  • src/menu-bar.js
  • src/shortcuts.js
  • src/state.js
  • tests/clipboard.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/menu-bar.js
  • src/shortcuts.js

📝 Walkthrough

Walkthrough

Adds session-scoped note copy, cut, and paste commands with normalized timing, cloned technique data, undo integration, menu and shortcut wiring, mode guards, tests, and changelog documentation.

Changes

Clipboard commands

Layer / File(s) Summary
Clipboard planning and editor actions
src/input.js, src/state.js
Adds note clipboard packing and paste planning, including timing normalization, technique-data cloning, missing-lane skipping, undo integration, selection updates, status reporting, and removal of the old state clipboard field.
Command and shortcut integration
src/input.js, src/shortcuts.js, src/menu-bar.js
Registers copy, cut, and paste commands, dispatches them through the editor, and exposes them in the Edit menu and shortcut profiles.
Behavior validation and release notes
tests/clipboard.test.mjs, CHANGELOG.md
Tests timing, cloning, lane filtering, undo/redo, cut persistence, selection, and mode guards; documents the new commands and bend-curve behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ShortcutRegistry
  participant EditorCommands
  participant NoteClipboard
  participant EditHistory

  User->>ShortcutRegistry: Ctrl+C, Ctrl+X, or Ctrl+V
  ShortcutRegistry->>EditorCommands: copySelection, cutSelection, or pasteAtPlayhead
  EditorCommands->>NoteClipboard: pack selection or plan paste
  NoteClipboard->>EditHistory: record delete or add operation
  EditHistory-->>User: update notes and selection
Loading

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: first-class Cut/Copy/Paste with visibility, snapping, guards, and bend-curve safety.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-clipboard

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/clipboard.test.mjs (1)

119-128: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Actually exercise the keys ↔ fretted guard.

This test only checks drum and tempo modes; it never changes track shape or asserts rejection. Add a copy from one track type followed by a paste attempt into the other.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/clipboard.test.mjs` around lines 119 - 128, Extend the “mode and shape
guards” test to exercise the keys↔fretted incompatibility: copy a selection from
one track shape, switch the active track to the other shape, and assert
_editorPasteAtPlayhead() returns false. Preserve the existing drumEditMode and
tempoMapMode rejection assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/input.js`:
- Around line 499-505: Update the Cut handling around the clipboard mutation to
reject recording mode, Parts view, and read-only fretted-roll state before
DeleteNotesCmd or success status; also update every Paste path in the
corresponding clipboard handler to apply the same guards, including same-track
writes, while preserving existing permitted behavior.

---

Nitpick comments:
In `@tests/clipboard.test.mjs`:
- Around line 119-128: Extend the “mode and shape guards” test to exercise the
keys↔fretted incompatibility: copy a selection from one track shape, switch the
active track to the other shape, and assert _editorPasteAtPlayhead() returns
false. Preserve the existing drumEditMode and tempoMapMode rejection assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7caec7da-47dc-4c78-9896-8026705fba31

📥 Commits

Reviewing files that changed from the base of the PR and between c85b7de and 2b46316.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/input.js
  • src/menu-bar.js
  • src/shortcuts.js
  • tests/clipboard.test.mjs

Comment thread src/input.js
ChrisBeWithYou and others added 3 commits July 14, 2026 16:23
…KeyDown

Review finding (community bot, valid): the registry made Cut/Paste menu-
and palette-invokable, which bypasses onKeyDown's mode gates — Cut/Paste
could mutate during MIDI recording, in the Tracks overview, and Paste
could add pitches to the read-only fretted roll on the SAME track.

_clipboardWriteBlocked() now gates every clipboard write inside the
commands themselves (recording / Tracks overview / read-only roll — the
same trio the right-click delete guard enforces); plain Copy stays free.
The cross-track-only roll check is replaced by the all-pastes guard.

Tests: the keys↔fretted refusal is now actually exercised in BOTH
directions (was asserted only by mode flags), and a new registry-path
case proves the overview blocks cut + paste while copy stays allowed —
it fails without the guard move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
PR #270 moved the note clipboard to a module-local _noteClipboard and
removed every reader/writer of S.clipboard, but left the now-dead
`clipboard: null` field in the session state. Remove it — zero refs
remain (grep-verified), and a stale field misleads the next reader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/clipboard.test.mjs (1)

61-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the negative-anchor clamp.

This test claims to validate t >= 0, but all calls use atTime = 10. A regression removing Math.max(0, ...) would still pass. Add a negative-anchor assertion.

🧪 Proposed test
   const bass = _clipboardPastePlanPure(clip, 10, 4);
   assert.strictEqual(bass.notes.length, 1);
   assert.strictEqual(bass.laneSkipped, 1);
+  const clamped = _clipboardPastePlanPure(clip, -2, 6);
+  assert.deepStrictEqual(clamped.notes.map(n => n.time), [0, 3]);
   assert.strictEqual(_clipboardPastePlanPure(null, 10, 6), null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/clipboard.test.mjs` around lines 61 - 69, Add an assertion in the test
covering _clipboardPastePlanPure that invokes it with a negative atTime and
verifies the resulting note times are clamped to zero or greater, specifically
exercising the Math.max(0, ...) behavior while preserving the existing retiming
and missing-string checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 57-58: Remove the duplicate ### Fixed heading in the Unreleased
changelog section and append the Shift Audio persistence and Resnap selection
entries to the existing ### Fixed list.
- Line 67: Remove the stray truncated “Inspector technique edits are undoable
now.” bullet fragment near the “Resnap selection” entry, while preserving the
complete existing version of that changelog item and all other list entries.
- Around line 142-143: Remove the duplicate Command palette bullet from
CHANGELOG.md, retaining only the entry that references View ▸ Command palette
and deleting the one that references Help ▸ Command palette.

---

Outside diff comments:
In `@tests/clipboard.test.mjs`:
- Around line 61-69: Add an assertion in the test covering
_clipboardPastePlanPure that invokes it with a negative atTime and verifies the
resulting note times are clamped to zero or greater, specifically exercising the
Math.max(0, ...) behavior while preserving the existing retiming and
missing-string checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: addaba90-99e2-4c24-b536-638abc23ce45

📥 Commits

Reviewing files that changed from the base of the PR and between 2b46316 and 421eca7.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/input.js
  • src/menu-bar.js
  • src/shortcuts.js
  • src/state.js
  • tests/clipboard.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/menu-bar.js
  • src/shortcuts.js

@coderabbitai coderabbitai Bot 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/clipboard.test.mjs (1)

61-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the negative-anchor clamp.

This test claims to validate t >= 0, but all calls use atTime = 10. A regression removing Math.max(0, ...) would still pass. Add a negative-anchor assertion.

🧪 Proposed test
   const bass = _clipboardPastePlanPure(clip, 10, 4);
   assert.strictEqual(bass.notes.length, 1);
   assert.strictEqual(bass.laneSkipped, 1);
+  const clamped = _clipboardPastePlanPure(clip, -2, 6);
+  assert.deepStrictEqual(clamped.notes.map(n => n.time), [0, 3]);
   assert.strictEqual(_clipboardPastePlanPure(null, 10, 6), null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/clipboard.test.mjs` around lines 61 - 69, Add an assertion in the test
covering _clipboardPastePlanPure that invokes it with a negative atTime and
verifies the resulting note times are clamped to zero or greater, specifically
exercising the Math.max(0, ...) behavior while preserving the existing retiming
and missing-string checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 57-58: Remove the duplicate ### Fixed heading in the Unreleased
changelog section and append the Shift Audio persistence and Resnap selection
entries to the existing ### Fixed list.
- Line 67: Remove the stray truncated “Inspector technique edits are undoable
now.” bullet fragment near the “Resnap selection” entry, while preserving the
complete existing version of that changelog item and all other list entries.
- Around line 142-143: Remove the duplicate Command palette bullet from
CHANGELOG.md, retaining only the entry that references View ▸ Command palette
and deleting the one that references Help ▸ Command palette.

---

Outside diff comments:
In `@tests/clipboard.test.mjs`:
- Around line 61-69: Add an assertion in the test covering
_clipboardPastePlanPure that invokes it with a negative atTime and verifies the
resulting note times are clamped to zero or greater, specifically exercising the
Math.max(0, ...) behavior while preserving the existing retiming and
missing-string checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: addaba90-99e2-4c24-b536-638abc23ce45

📥 Commits

Reviewing files that changed from the base of the PR and between 2b46316 and 421eca7.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/input.js
  • src/menu-bar.js
  • src/shortcuts.js
  • src/state.js
  • tests/clipboard.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/menu-bar.js
  • src/shortcuts.js
🛑 Comments failed to post (3)
CHANGELOG.md (3)

57-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicate ### Fixed heading within the same Unreleased block.

Line 57 introduces a second ### Fixed heading while one already exists (unmarked) at line 25, both inside the same ## [Unreleased] section — this is exactly what the markdownlint MD024 warning flags. The new fix bullets that follow (Shift Audio persistence, Resnap selection) should be appended to the existing ### Fixed list instead of starting a new heading.

📝 Proposed fix
   recording and your notes ride along.
 
-### Fixed
-
 - **"Shift Audio…" now survives saving and reopening.** The recording-vs-chart
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.


🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 57-57: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 57 - 58, Remove the duplicate ### Fixed heading in
the Unreleased changelog section and append the Shift Audio persistence and
Resnap selection entries to the existing ### Fixed list.

Source: Linters/SAST tools


67-67: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stray duplicate bullet fragment breaks the list.

Line 67 is a truncated copy of the "Inspector technique edits are undoable now." bullet (Toggling a technique flag with no continuation) inserted right before the new "Resnap selection" bullet. The full, correct version of this exact bullet already exists further down (unchanged, lines 79-86), so line 67 is a stray leftover that renders as a broken, unfinished list item.

📝 Proposed fix
   byte-identical to before.
-- **Inspector technique edits are undoable now.** Toggling a technique flag
 - **Resnap selection now works with Snap toggled off — and snaps both edges.**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 67, Remove the stray truncated “Inspector technique
edits are undoable now.” bullet fragment near the “Resnap selection” entry,
while preserving the complete existing version of that changelog item and all
other list entries.

142-143: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the actual menu path that opens the command palette.
rg -n -i "command palette" src/menu-bar.js

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- CHANGELOG.md around lines 132-150 ---'
sed -n '132,150p' CHANGELOG.md

echo
echo '--- repo-wide matches for command palette / menu locations ---'
rg -n -i 'command palette|View ▸ Command palette|Help ▸ Command palette' .

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 3761


Remove the stray duplicate Command palette bullet

Drop the extra line at CHANGELOG.md:142; the command palette belongs under View ▸ Command palette, not Help ▸ Command palette.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 142 - 143, Remove the duplicate Command palette
bullet from CHANGELOG.md, retaining only the entry that references View ▸
Command palette and deleting the one that references Help ▸ Command palette.

@byrongamatos
byrongamatos merged commit 67ec724 into main Jul 15, 2026
4 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-clipboard branch July 15, 2026 20:22
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.

2 participants