Skip to content

Commit 387b25f

Browse files
committed
feat(editor): Multi-Cursor und Column Selection Modus implementieren
1 parent 992ad6c commit 387b25f

7 files changed

Lines changed: 1250 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@
99
surface exists.
1010

1111
Alle wesentlichen Änderungen an CodeBox werden hier dokumentiert.
12+
13+
## [0.1.6] - 2026-09-09
14+
15+
### Multi-Cursor & Column Selection Modus (2026-09-09)
16+
17+
- `core/multi_cursor.py`: Neues Modul für Multi-Cursor- und Spaltenauswahl-Verwaltung:
18+
- `MultiCursorManager`: Verwaltet sekundäre Cursoren (`secondary_cursors`), synchrone Eingaben, Cursor-Merging und Selektionen.
19+
- Methoden `add_cursor_at`, `toggle_cursor_at`, `add_cursor_above` (`Ctrl+Alt+Up`), `add_cursor_below` (`Ctrl+Alt+Down`).
20+
- `select_all_occurrences` (`Ctrl+Shift+L`): Markiert alle Vorkommen des aktuellen Worts oder der Auswahl mit synchronen Cursorn.
21+
- `add_next_occurrence` (`Ctrl+Alt+L`): Fügt das nächste Vorkommen sukzessive zur Mehrfachauswahl hinzu.
22+
- `column_select_between_cursors`: Rechteckige Spaltenauswahl über mehrere Zeilen via `Alt+Shift+Drag`.
23+
- Synchrone Textmanipulation: `insert_text`, `delete_backspace`, `delete_forward`, `unindent_cursors`, Auto-Pairing & Bracket-Wrapping (`insert_pair_or_wrap`).
24+
- Multi-Cursor Zwischenablage: `copy_selection`, `cut_selection` und zeilenweises `paste_text`.
25+
- Atomares Undo/Redo: Alle Änderungen über mehrere Cursoren hinweg werden in einem einzigen `beginEditBlock()`/`endEditBlock()` gekapselt.
26+
- Visuelles Rendering: `get_extra_selections()` mit `#264f78` und 2px scharfe Vektor-Carets in `paint_extra_carets()`.
27+
- `core/editor.py`:
28+
- Integration von `MultiCursorManager`.
29+
- `paintEvent`: Rendert sekundäre Carets im Viewport.
30+
- `mousePressEvent` / `mouseMoveEvent` / `mouseReleaseEvent`: `Alt+Klick` zum Setzen/Entfernen von Cursorn und `Alt+Shift+Ziehen` für Spaltenauswahl.
31+
- `keyPressEvent`: Tastatur-Shortcuts und synchrone Multi-Cursor-Tastendeligation.
32+
- `highlightCurrentLine`: Dynamische Einbindung sekundärer Selektionen.
33+
- `ui/main_window.py`:
34+
- Untermenü *Mehrfachauswahl & Multi-Cursor* im Menü *Bearbeiten*.
35+
- Statusleistenanzeige: Anzeige von ` (X Cursor)` bei aktiven Mehrfachcursorn.
36+
- `ui/shortcuts_dialog.py`: 6 neue Tastenkürzel für Multi-Cursor dokumentiert.
37+
- `tests/test_multi_cursor.py`: 15 automatisierte Tests für Cursor-Erstellung, Spaltenauswahl, parallele Eingabe, Löschen, Navigation, Merging, Copy/Paste, Undo/Redo und UI-Aktionen (195 passed, 1 skipped).
38+
1239
## [0.1.5] - 2026-09-09
1340

1441
### Geteilte Editor-Ansichten (Split Editor Horizontal / Vertikal) (2026-09-09)

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
[![Python 3.10+](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/)
77
[![Platform: Windows | Linux | macOS](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
88
[![CI](https://github.com/dev-bricks/CodeBox/actions/workflows/ci.yml/badge.svg)](https://github.com/dev-bricks/CodeBox/actions/workflows/ci.yml)
9-
[![Tests](https://img.shields.io/badge/tests-164%20passed%20%7C%20100%25-brightgreen.svg)]()
9+
[![Tests](https://img.shields.io/badge/tests-195%20passed%20%7C%20100%25-brightgreen.svg)]()
1010
[![Privacy: Zero-Egress](https://img.shields.io/badge/privacy-100%25%20local--first%20%7C%20zero--egress-success.svg)](SECURITY.md)
1111
[![Security Policy](https://img.shields.io/badge/security-bilingual%20policy-blue.svg)](SECURITY.md)
1212
[![Ecosystem: dev-bricks](https://img.shields.io/badge/ecosystem-dev--bricks-blue.svg)](https://github.com/dev-bricks)
1313
[![Part of: open-bricks](https://img.shields.io/badge/part%20of-open--bricks-blue.svg)](https://github.com/open-bricks)
1414
[![LSP Ready](https://img.shields.io/badge/LSP-ready-purple.svg)]()
15-
[![Version: 0.1.2](https://img.shields.io/badge/version-0.1.2-green.svg)](CHANGELOG.md)
15+
[![Version: 0.1.6](https://img.shields.io/badge/version-0.1.6-green.svg)](CHANGELOG.md)
1616
[![llms.txt](https://img.shields.io/badge/llms.txt-available-green.svg)](llms.txt)
1717

1818
[Deutsch](README_de.md) | English
@@ -180,6 +180,8 @@ sequenceDiagram
180180

181181
## Features
182182

183+
- **Multi-Cursor & Column Selection**: Edit multiple document locations simultaneously, column selection (`Alt+Shift+Drag`), occurrence tagging (`Ctrl+Shift+L` / `Ctrl+Alt+L`), auto-pair wrapping, and atomic multi-cursor undo/redo.
184+
- **Code Folding & Split Editor**: Interactive code folding with gutter indicators and side-by-side or stacked split panes with synchronized buffers.
183185
- **Rich Syntax Highlighting**: Pre-configured highlighting for Python, JavaScript, TypeScript, C++, Rust, Go, and Java with punctuation-safe word boundary matching.
184186
- **Declarative Plugin Architecture**: Create and extend language definitions in seconds using clean JSON schemas (`plugins/`, `~/.codebox/plugins/`).
185187
- **Interactive Management Dialogs**: Full GUI dialogs for managing language plugins (`Ctrl+Shift+P`) and reviewing keyboard shortcuts (`F1`).

core/editor.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from core.folding import FoldingManager
16+
from core.multi_cursor import MultiCursorManager
1617

1718

1819
class LineNumberArea(QWidget):
@@ -259,6 +260,8 @@ def __init__(self, parent=None):
259260

260261
self.FOLD_AREA_WIDTH = 14
261262
self.folding_manager = FoldingManager(self)
263+
self.multi_cursor_manager = MultiCursorManager(self)
264+
self._column_drag_start = None
262265
self._fold_timer = QTimer(self)
263266
self._fold_timer.setSingleShot(True)
264267
self._fold_timer.setInterval(100)
@@ -564,6 +567,45 @@ def move_line_down(self):
564567
new_cursor.setPosition(nxt.position() + min(col, len(curr_text)))
565568
self.setTextCursor(new_cursor)
566569

570+
def paintEvent(self, event):
571+
super().paintEvent(event)
572+
if hasattr(self, 'multi_cursor_manager') and self.multi_cursor_manager.has_extra_cursors():
573+
self.multi_cursor_manager.paint_extra_carets(self)
574+
575+
def mousePressEvent(self, event):
576+
if event.button() == Qt.MouseButton.LeftButton:
577+
modifiers = event.modifiers()
578+
if modifiers & Qt.KeyboardModifier.AltModifier:
579+
if modifiers & Qt.KeyboardModifier.ShiftModifier:
580+
# Spaltenauswahl via Alt+Shift+Mausklick / Drag starten
581+
self._column_drag_start = self.cursorForPosition(event.position().toPoint())
582+
event.accept()
583+
return
584+
else:
585+
# Weiteren Cursor an Klickposition setzen oder entfernen
586+
clicked_c = self.cursorForPosition(event.position().toPoint())
587+
self.multi_cursor_manager.toggle_cursor_at(clicked_c.position())
588+
event.accept()
589+
return
590+
elif hasattr(self, 'multi_cursor_manager') and self.multi_cursor_manager.has_extra_cursors():
591+
self.multi_cursor_manager.clear()
592+
super().mousePressEvent(event)
593+
594+
def mouseMoveEvent(self, event):
595+
if getattr(self, '_column_drag_start', None) is not None:
596+
end_cursor = self.cursorForPosition(event.position().toPoint())
597+
self.multi_cursor_manager.column_select_between_cursors(self._column_drag_start, end_cursor)
598+
event.accept()
599+
return
600+
super().mouseMoveEvent(event)
601+
602+
def mouseReleaseEvent(self, event):
603+
if getattr(self, '_column_drag_start', None) is not None:
604+
self._column_drag_start = None
605+
event.accept()
606+
return
607+
super().mouseReleaseEvent(event)
608+
567609
def keyPressEvent(self, event):
568610
# Completer aktiv?
569611
if self.completer and self.completer.popup().isVisible():
@@ -572,6 +614,34 @@ def keyPressEvent(self, event):
572614
event.ignore()
573615
return
574616

617+
# Multi-Cursor Shortcuts
618+
modifiers = event.modifiers()
619+
if (modifiers & Qt.KeyboardModifier.ControlModifier) and (modifiers & Qt.KeyboardModifier.AltModifier):
620+
if event.key() == Qt.Key.Key_Up:
621+
self.multi_cursor_manager.add_cursor_above()
622+
return
623+
if event.key() == Qt.Key.Key_Down:
624+
self.multi_cursor_manager.add_cursor_below()
625+
return
626+
if event.key() == Qt.Key.Key_L:
627+
self.multi_cursor_manager.add_next_occurrence()
628+
return
629+
630+
if (modifiers & Qt.KeyboardModifier.ControlModifier) and (modifiers & Qt.KeyboardModifier.ShiftModifier):
631+
if event.key() == Qt.Key.Key_L:
632+
self.multi_cursor_manager.select_all_occurrences()
633+
return
634+
635+
# Escape hebt Multi-Cursor auf
636+
if event.key() == Qt.Key.Key_Escape and self.multi_cursor_manager.has_extra_cursors():
637+
self.multi_cursor_manager.clear()
638+
return
639+
640+
# Multi-Cursor Tasten-Handling bei aktiven Sekundär-Cursorn
641+
if self.multi_cursor_manager.has_extra_cursors():
642+
if self.multi_cursor_manager.handle_key_press(event):
643+
return
644+
575645
# Tab & Shift+Tab / Backtab (Einrücken / Ausrücken)
576646
if event.key() == Qt.Key.Key_Backtab or (
577647
event.key() == Qt.Key.Key_Tab and bool(event.modifiers() & Qt.KeyboardModifier.ShiftModifier)
@@ -901,6 +971,8 @@ def highlightCurrentLine(self):
901971
extraSelections = (list(self.search_selections) +
902972
list(self.bracket_selections) +
903973
list(self.error_selections))
974+
if hasattr(self, 'multi_cursor_manager'):
975+
extraSelections.extend(self.multi_cursor_manager.get_extra_selections())
904976
if not self.isReadOnly():
905977
selection = QTextEdit.ExtraSelection()
906978
selection.format.setBackground(QColor(45, 45, 45))

0 commit comments

Comments
 (0)