collective

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

diff --git a/crates/engine/src/cmd/midi.rs b/crates/engine/src/cmd/midi.rs--- a/crates/engine/src/cmd/midi.rs+++ b/crates/engine/src/cmd/midi.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();diff --git a/crates/ui-egui/src/midi_editor.rs b/crates/ui-egui/src/midi_editor.rs--- a/crates/ui-egui/src/midi_editor.rs+++ b/crates/ui-egui/src/midi_editor.rs@@ -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()