Skip to content

Feature/album issue 70 - #72

Merged
tinchodias merged 4 commits into
pharo-graphics:devfrom
koendehondt:feature/album-issue-70
Oct 30, 2026
Merged

tinchodias merged 4 commits into
pharo-graphics:devfrom
koendehondt:feature/album-issue-70

Conversation

@koendehondt

@koendehondt koendehondt commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

This PR replaces #71, which started from the wrong branch.

Adds VS Code-style multi-cursor editing to the Album text editor, as requested by #70.

Try it

AlbTextEditorExamples exampleMultiCursorEditing opens an editor with a cursor at the start of every line.

Gesture Effect
Alt-click (Option-click on macOS) Toggle a cursor at the clicked position
Ctrl+Alt+Up/Down (Cmd+Option+Up/Down on macOS) Add a cursor above the top-most / below the bottom-most cursor
Shift+arrows, Shift+Alt+arrows, Shift+Home/End Extend or shrink the selection at every cursor
Copy / Cut One line per selection
Paste One line per cursor when the line count matches, otherwise the whole text at every cursor
Escape Keep only the primary cursor
Plain click, select all, mouse drag A single cursor / selection

Changes

Cursors

  • AlbTextEditor keeps its primary cursor and gains secondaryCursors. cursors returns all cursors sorted by position. New: addCursorAt:, removeSecondaryCursors, hasMultipleCursors.
  • AlbTextEditorNavigator:
    • Moves (left/right, up/down, word, line start/end, moveBy:, moveWithin:) apply to every cursor.
    • moveTo: / moveToScreenPosition: collapse to a single cursor; addAt: keeps its existing meaning.
    • New addCursorAt:, removeCursorAt:, removeSecondaryCursors; removeAt: (sent by the history but previously missing) is implemented.
    • apply merges cursors at the same position. Removing the primary cursor promotes a secondary one; the last cursor is never removed.
  • The inserter, deleter and paster act at every cursor. Enter keeps the indentation of each cursor's line. A multi-interval deletion is one undoable step. Cursor-movement undo/redo restores every cursor.

Selections

  • AlbTextEditorSelecter: keyboard extensions build one command per cursor (private...For: methods) and move all cursors with a single navigator.
  • Selections that meet merge; one cursor survives, at the selection edge (withMergingWithinSelection, triggered at the end of select once the editor selection is up to date).
  • Typing, paste and deletion replace every selection. Explicit ranges (select all, drag, Shift-click) keep a single cursor.

Copy / paste

  • New AlbTextEditor>>selectedTexts. copySelected copies one selection per line; cutSelected removes only the selected ranges. Previously, with several selections, they used the span from the first to the last selection, so cut deleted the unselected text in between.
  • AlbTextEditorPaster>>paste distributes lines over cursors (VS Code rule; a trailing line ending does not count).

Rendering

  • apply is two-phase: remove all cursors from their old segments, update the model, then re-add them, so cursors sharing a segment do not hide each other.
  • Segment pieces track all their local cursors. AlbTextElement holds secondary cursor elements next to the editor's shared primary cursor element.

Shortcuts and mouse

  • New AlbEditorAddCursorAboveShortcut / AlbEditorAddCursorBelowShortcut; Escape (AlbEditorDeselectAllShortcut) also removes secondary cursors; Alt/Option-click in AlbTextEditorMouseHandler.

Cleanup

  • Removed the unused toRemoveCursors slot from AlbExtendSelectionCommand and commented-out code from privatePerformSelectAndDeselectCommands:.

Tests

  • New AlbMultiCursorTest (46 tests), a BlSpaceTestCase: element-based tests run in the test space with space deferAndSettle: (newer Bloc forbids forceLayout on attached elements). The notation 'a|b|c' means the text abc with cursors at 1 and 2. Coverage:
    • cursor-set invariants
    • insertion and deletion
    • navigation
    • undo/redo
    • selections: extending, shrinking, merging, typing over
    • copy/paste/cut
    • rendering
    • shortcuts
    • two seeded random oracle tests
  • AlbTextEditorHistoryTest: two redo regression tests for multi-index insertion. One of them wraps a selection in parentheses, then undoes and redoes, a pre-existing single-cursor bug.
  • Tests touching the system clipboard restore it afterwards.
  • Album-Tests: all multi-cursor and existing tests pass. AlbTextEditorByScripterTest (upstream) needs the Bloc-Simulation package, which the full Bloc baseline loads.

Dependency

Requires pharo-graphics/Bloc# (requested indices in BlTextStringsInserted, deletion in BlSelectionTree). Without it, the multi-index redo tests and two selection tests fail.

See pharo-graphics/Bloc#895

Limitations

  • Cursors don't blink by default (AlbCursorElement>>defaultBlinking is false). When blinking is enabled with cursorElement beBlinking: true, only the primary cursor blinks; synchronized blinking will come in a separate PR.

Keyboard selection (Shift+arrows, word and line start/end) now
extends or shrinks a selection at every cursor instead of collapsing
to the primary one. Typing, paste and deletion apply to every
selection.

- AlbTextEditorSelecter: extension operations build one command per
  cursor (new private...For: methods) and move all cursors with a
  single navigator apply
- AlbTextEditorNavigator: optional merging of cursors within the same
  selection (withMergingWithinSelection), keeping one cursor at the
  selection edge; the selecter triggers it once the editor selection
  is up to date
- Explicit ranges (select all, mouse drag, Shift-click) still keep a
  single cursor

Requires the BlSelectionTree>>delete: fix in Bloc.

Replace test_selecting_keeps_only_the_primary_cursor with 10 selection
tests in AlbMultiCursorTest.
Paste: when there are several cursors and the pasted text has exactly
as many lines, each cursor gets one line, in text order. Otherwise the
whole text is pasted at every cursor, as before. A trailing line
ending does not count as an extra line.

Copy/cut: copySelected and cutSelected used the span from the first
to the last selection, so with several selections they copied (and
cut) the unselected text in between. Each selection is now copied
separately, one per line (new AlbTextEditor>>selectedTexts), and cut
removes only the selected ranges. A single selection behaves as
before.

Cleanup:
- AlbTextEditorNavigator: initialize shouldMergeWithinSelection to
  false instead of testing it against true
- AlbTextEditorSelecter: remove commented-out code from
  privatePerformSelectAndDeselectCommands:
- AlbExtendSelectionCommand: remove the unused toRemoveCursors slot
- Formatting of the new private...For: methods in AlbTextEditorSelecter

Add 8 copy/paste tests to AlbMultiCursorTest. Tests touching the
system clipboard restore it afterwards.
Newer Bloc forbids #forceLayout on elements attached to a space.
AlbMultiCursorTest now subclasses BlSpaceTestCase: element-based tests
add the editor to the test space and perform edits within
space deferAndSettle:, and cursor elements are collected after
space settle.
@koendehondt koendehondt mentioned this pull request Oct 30, 2026
@tinchodias
tinchodias merged commit 44f8e4e into pharo-graphics:dev Oct 30, 2026
2 of 4 checks passed
@tinchodias

Copy link
Copy Markdown
Collaborator

Thanks

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