Skip to content

refactor(editor): extract the keys / piano-roll model to src/keys.js (R2, step 8b) - #155

Merged
byrongamatos merged 1 commit into
mainfrom
refactor/es-module-split-keys
Jul 9, 2026
Merged

refactor(editor): extract the keys / piano-roll model to src/keys.js (R2, step 8b)#155
byrongamatos merged 1 commit into
mainfrom
refactor/es-module-split-keys

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Step 8b of the editor's ES-module split (R2). main.js drops under 20k for the first time: 20,127 → 19,963.

What moved

src/keys.js (206 lines) — everything about the piano roll and which view a part opens in.

  • view resolution: viewFor, isKeysMode, isKeysArr, _rollReadOnly, plus the persisted per-part preference (_viewPrefs, _viewPrefsSave, _partViewKeyPure, _viewForPure, KEYS_PATTERN)
  • roll geometry: midiToY, yToMidi, pianoLaneCount, pianoRange, PIANO_LANE_H, updatePianoRange, PIANO_OCTAVE_COLORS
  • MIDI helpers: noteToMidi, midiToNote, midiToString, midiToFret, isBlackKey, midiToFreq, _inKeyboardGutterPure
  • sounding-pitch context that renders a fretted part read-only in the roll: _rollPitchCtx, _rollMidiForNote

_rollLockNotice stays behind — it calls setStatus. main.js's entire diff is the deletions plus the import block. Graph stays acyclic: keys → {geometry, lanes, notes, state, theory}.

Live bindings, not a container

PIANO_LANE_H and pianoRange are reassigned per arrangement. But their sole writer, updatePianoRange, moved with them — so they're export let, importers read them live, and an importer that tries to write gets a TypeError.

That's the step-5 rule (geometry's lane metrics), not the step-4 one (lanes.js's LC, whose writers draw() and onMouseMove() had to stay behind). The question is always "can the writer move?", never "is it reassigned?". No container, no rename: main.js's 24 PIANO_LANE_H and 9 pianoRange references are untouched.

Tests — four more off the slicer path

  • keyboard_gutter, keyboard_gutter_dblclick@pure:midi-freq retired; both import midiToFreq / _inKeyboardGutterPure. dblclick still brace-extracts onDblClick, which stays in main.js.
  • rename_part — was regex-lifting const KEYS_PATTERN = … straight out of the source text. Now imports it and injects it into the @pure:rename-arr sandbox.
  • view_switcher — the real rework. It used to re-evaluate _viewPrefs/viewFor/isKeysArr/isKeysMode/_rollReadOnly/updatePianoRange from source inside a new Function sandbox, against a fabricated S and an injected localStorage. It now drives the real module against the real S, installs the stub on globalThis (module code resolves localStorage at call time), bounces S.filename to bust _viewPrefs' per-song memo between cases, and reads pianoRange back through the namespace — a destructured copy would go stale, so that read is what proves the live binding actually updates for importers.

Verification

  • node --test 86/86, pytest 248/248.

  • No surviving assignment to PIANO_LANE_H/pianoRange anywhere in main.js — one would now throw. The only writers are the four lines inside updatePianoRange. No unused import; _viewForPure is the single test-only export.

  • A fifth headless harness, written for this step, because no existing one ever opened the roll — draw, hit-test and resize all use a 6-string guitar chart. It discriminates the two views by what they paint: a fretted part draws bold-12px lane labels; a roll draws one keyboard-gutter fillRect per semitone of pianoRange.

    view lane labels gutter rows
    Lead (fretted) E A D G B e 0
    Keys 60
    Lead forced into the roll 60
    back to string view E A D G B e 0

    That row count is the assertion: 60 rows means the roll rendered the fretted part at sounding pitch. The wire packing string*24 + fret would span roughly 140 rows for the same notes. (The C-row octave labels are unusable as a signal — drawPianoLabels only draws them when PIANO_LANE_H >= 7, and a wide range puts it at 4–6px. Cost me a probe to find out.)

  • All four existing harnesses green. Codex preflight: NO ISSUES.

Next

keys.js was what Drawing (1,046 lines) and Hit-testing were coupled to. Step 9 can take them, once canvas/ctx are lifted — and their writer is a single init() assignment from document.getElementById, so by the rule above it moves as setCanvas(el) and they too become live bindings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved piano-roll and keys view behavior, including smarter per-part view selection and saved view preferences.
    • Piano range now updates more accurately based on the notes in the current part.
  • Bug Fixes

    • Refined note-to-piano-roll positioning and pitch mapping for more consistent display and interaction.
    • Keyboard gutter interactions now behave more reliably across views.
  • Tests

    • Updated automated checks for view switching, piano range, and keyboard gutter behavior.

…(R2, step 8b)

src/main.js 20,127 -> 19,963 — under 20k for the first time. keys.js holds which
view a part opens in (viewFor, isKeysMode, isKeysArr, _rollReadOnly) and the
persisted per-part preference behind it, the roll's MIDI<->y geometry (midiToY,
yToMidi, pianoLaneCount, noteToMidi/midiToNote/midiToString/midiToFret, isBlackKey,
midiToFreq, PIANO_OCTAVE_COLORS, KEYS_PATTERN), and the sounding-pitch context that
renders a fretted part read-only in the roll (_rollPitchCtx, _rollMidiForNote).
_rollLockNotice stays in main.js — it calls setStatus.

main.js's whole diff is the deletions plus the import block. Graph stays acyclic:
keys -> {geometry, lanes, notes, state, theory}.

LIVE BINDINGS, not a container. PIANO_LANE_H and pianoRange are reassigned per
arrangement, but their SOLE writer — updatePianoRange — moved with them, so they
are `export let` and importers read them live and cannot write them. That is the
step-5 rule (geometry's lane metrics), not the step-4 one (lanes.js's LC, whose
writers had to stay in draw()/onMouseMove()). The question is always "can the
writer move?", not "is it reassigned?".

Tests: four more suites off the slicer path.
  - keyboard_gutter, keyboard_gutter_dblclick: @pure:midi-freq is gone; both
    import midiToFreq / _inKeyboardGutterPure. dblclick still brace-extracts
    onDblClick, which stays in main.js.
  - rename_part: was regex-lifting `const KEYS_PATTERN = ...` out of the source;
    now imports it and injects it into the @pure:rename-arr sandbox.
  - view_switcher: the real rework. It used to re-evaluate _viewPrefs/_viewPrefsSave/
    viewFor/isKeysArr/isKeysMode/_rollReadOnly/updatePianoRange from SOURCE inside a
    sandbox, against a fabricated `S` and an injected localStorage stub. It now
    drives the real module against the real S, installs the stub on globalThis
    (module code resolves `localStorage` at call time), bounces S.filename to bust
    _viewPrefs' per-song memo between cases, and reads pianoRange back through the
    NAMESPACE — a destructured copy would go stale, so that read is what proves the
    live binding actually updates for importers.

Verified: node --test 86/86, pytest 248/248. main.js diff mechanically checked to
be deletions + the import block. No assignment to PIANO_LANE_H/pianoRange survives
in main.js (it would now throw); the only writers are the four lines inside
updatePianoRange. No unused import; _viewForPure is the one test-only export.

FIFTH headless harness, written for this step: no existing harness opened the roll
(they all use a 6-string guitar chart). It discriminates the two views by what they
paint — a fretted part draws bold-12px lane labels, a roll draws one keyboard-gutter
fillRect per semitone of pianoRange. On Arcturus: Lead → 6 lane labels / 0 gutter
rows; Keys → 0 labels / 60 rows; Lead forced into the roll via the view pref → 60
rows, i.e. SOUNDING pitch (the wire packing string*24+fret would span ~140 rows);
switching back restores the lanes. The C-row octave labels are not usable as a
signal — drawPianoLabels only draws them when PIANO_LANE_H >= 7, and a wide range
puts it at 4-6px. All four existing harnesses green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 12:29
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dccfa8d9-3952-4847-b4f8-cd48878f9bdf

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5a90d and cd11cdc.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/keys.js
  • src/main.js
  • tests/keyboard_gutter.test.mjs
  • tests/keyboard_gutter_dblclick.test.mjs
  • tests/rename_part.test.mjs
  • tests/view_switcher.test.mjs

📝 Walkthrough

Walkthrough

This PR extracts piano-roll/keys logic from src/main.js into a new src/keys.js module, exporting constants, view-preference persistence, mode predicates, pitch/MIDI conversion helpers, and updatePianoRange. main.js now imports these instead of defining them inline. Tests are converted to ESM and updated to import real module exports directly rather than extracting source from main.js. CHANGELOG documents the migration step.

Changes

Keys/piano-roll model extraction

Layer / File(s) Summary
Keys module constants and view-preference logic
src/keys.js
Exports piano-roll constants (colors, live PIANO_LANE_H/pianoRange bindings, KEYS_PATTERN), per-part view key derivation, and localStorage-backed view preference caching/saving/resolution.
Mode predicates and pitch context
src/keys.js
Adds isKeysArr, isKeysMode, _rollReadOnly predicates and _rollPitchCtx for fretted-part sounding-pitch context.
MIDI/geometry conversions and piano range recompute
src/keys.js
Adds MIDI↔note/frequency/string/fret/Y conversions, keyboard-gutter hit-testing, and updatePianoRange to recompute range/lane height from arrangement notes.
main.js delegates to keys.js
src/main.js
Removes in-file piano-roll definitions, imports the corresponding items from ./keys.js, and relocates _rollLockNotice().
Keyboard gutter tests use real exports
tests/keyboard_gutter.test.mjs, tests/keyboard_gutter_dblclick.test.mjs
Converts to ESM loading and imports midiToFreq/_inKeyboardGutterPure directly from src/keys.js instead of extracting them from src/main.js.
Rename-part test uses imported KEYS_PATTERN
tests/rename_part.test.mjs
Converts to ESM loading and imports KEYS_PATTERN from src/keys.js instead of regex-scanning src/main.js.
View switcher test drives real module
tests/view_switcher.test.mjs
Imports helpers from src/keys.js, rewrites makeViewEnv to seed the real S/stub localStorage, and validates live pianoRange/PIANO_LANE_H bindings via updatePianoRange().
Changelog entry
CHANGELOG.md
Documents the ES-module migration step 8b for the keys/piano-roll model.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: extracting the keys/piano-roll model into src/keys.js.
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 refactor/es-module-split-keys

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
src/main.js

ast-grep timed out on this file


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

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR continues the ES-module migration by extracting the keys/piano-roll model out of src/main.js into a dedicated src/keys.js module and updating tests to import real implementations instead of slicing/eval’ing source blocks.

Changes:

  • Added src/keys.js containing view-pref logic, roll pitch mapping, and piano-roll geometry/state.
  • Updated src/main.js to import keys/piano-roll constants and functions from src/keys.js.
  • Updated multiple test suites to use real module imports and simplified extraction/sandboxing; updated changelog entry.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/view_switcher.test.mjs Switches from source-slicing to importing src/keys.js, stubs globalThis.localStorage, and asserts live export bindings.
tests/rename_part.test.js Migrates test to ESM-style imports and uses real KEYS_PATTERN.
tests/keyboard_gutter_dblclick.test.js Migrates test to ESM-style imports and uses real _inKeyboardGutterPure.
tests/keyboard_gutter.test.js Migrates test to ESM-style imports and uses real midiToFreq / _inKeyboardGutterPure.
src/main.js Removes inlined keys/piano-roll code and imports equivalents from src/keys.js.
src/keys.js New module implementing keys/piano-roll model, including persisted view prefs and roll geometry helpers.
CHANGELOG.md Documents the extraction/migration and the updated testing approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// keys.js reads the real `S` and the ambient `localStorage`. Install a stub on
// globalThis (module code resolves it at call time) and seed the real S, instead
// of re-evaluating the module's source inside a sandbox.
function makeViewEnv(seedS, seed = {}) {
Comment on lines 74 to 79
const map = new Map(Object.entries(seed));
const ls = {
globalThis.localStorage = {
getItem: k => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => { map.set(k, String(v)); },
removeItem: k => { map.delete(k); },
map,
};
Comment thread src/keys.js
Comment on lines +55 to +63
export function _viewPrefs() {
const key = 'editorViewPref:' + (S.filename || '');
if (_viewPrefFor === key && _viewPrefCache) return _viewPrefCache;
_viewPrefFor = key;
_viewPrefCache = {};
// Unsaved songs don't read a bare slot every unsaved song would share.
if (!S.filename) return _viewPrefCache;
try {
const raw = localStorage.getItem(key);
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