Browse / github.com/storytold/soundcraft / patch
Keep the selection on moved notes after arrow-key moves
While moving notes with arrow keys in MIDI Editor selection jumps to another note ### Description When attempting to move a single note in the MIDI editor using arrow the keys. Once you move the note past another note it unexpectedly jumps to the note you've passed. ### Environment SoundCraft Version 0.7.0 Windows 11 demo session ### Steps to reproduce 1. `soundcraft --demo`, then Window › MIDI Editor and select the Lead clip. 2. Select a note. 3. Move the note to the right with **arrow keys** so that it goes past another note. ### Expected The selection stays on the same note you selected. ### Actual Once you move the note past another note, the selection jumps
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #778, open)
- Base
2e9ad2f4215bd36e7edf30579e8ce520d5ce67c7- Size
- +30 −3 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
2b6c4661f517f29a2e3aa283
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/midi.rs crates/ui-egui/src/midi_editor.rs
@@ -124,14 +124,24 @@ fn notes_move(e: &mut Engine, p: &Value) -> Result<Value> { let dt = i64_or(p, "ticks", 0); let ds = i64_or(p, "semitones", 0); let seq = seq_mut(e, id, "midi.notes_move")?;+ let mut moved = Vec::new(); for (i, n) in seq.notes.iter_mut().enumerate() { if idx.contains(&i) { n.start = (n.start + dt).max(0); n.pitch = (i64::from(n.pitch) + ds).clamp(0, 127) as u8;+ moved.push(*n); } } seq.sort();- Ok(json!({"moved": idx.len()}))+ // Where the moved notes ended up after sorting, so callers can keep them selected.+ let mut new_idx: Vec<usize> = Vec::new();+ for m in &moved {+ if let Some(j) = seq.notes.iter().enumerate().position(|(j, x)| x == m && !new_idx.contains(&j)) {+ new_idx.push(j);+ }+ }+ new_idx.sort_unstable();+ Ok(json!({"moved": idx.len(), "indices": new_idx})) } #[cfg(test)]@@ -157,6 +167,19 @@ mod tests { assert!(e.execute("midi.note_edit", &json!({"clip": clip, "index": 99})).is_err()); } + #[test]+ fn notes_move_reports_new_indices_of_moved_notes() {+ let mut e = Engine::default();+ let t = e.session_mut().add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some("Keys"));+ let clip = e.execute("midi.new_clip", &json!({"track": t.0})).unwrap()["clip"].as_u64().unwrap();+ e.execute("midi.note_add", &json!({"clip": clip, "pitch": 60, "start_ticks": 0})).unwrap();+ e.execute("midi.note_add", &json!({"clip": clip, "pitch": 64, "start_ticks": 480})).unwrap();+ let r = e.execute("midi.notes_move", &json!({"clip": clip, "indices": [0], "ticks": 960})).unwrap();+ assert_eq!(r["indices"], json!([1]));+ let n = e.execute("midi.notes", &json!({"clip": clip})).unwrap();+ assert_eq!(n["notes"][1]["pitch"], 60);+ }+ #[test] fn rejects_audio_tracks() { let mut e = Engine::default();@@ -345,8 +345,12 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) { } else { 0 };- if (semi != 0 || ticks != 0) && !app.midi.selected.is_empty() {- let _ = app.run("midi.notes_move", json!({"clip": cid.0, "indices": app.midi.selected, "ticks": ticks, "semitones": semi}));+ if (semi != 0 || ticks != 0)+ && !app.midi.selected.is_empty()+ && let Ok(r) = app.run("midi.notes_move", json!({"clip": cid.0, "indices": app.midi.selected, "ticks": ticks, "semitones": semi}))+ && let Some(a) = r.get("indices").and_then(|v| v.as_array())+ {+ app.midi.selected = a.iter().filter_map(|v| v.as_u64()).map(|v| v as usize).collect(); } } if !app.midi.selected.is_empty()