Skip to content

Commit 9c274a0

Browse files
committed
feat: Add visual breakpoint gutter (F9), recent files menu, and bump version 0.3.2
1 parent 85adc9c commit 9c274a0

10 files changed

Lines changed: 397 additions & 63 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@
1010

1111
Alle wesentlichen Änderungen an CodeBox werden hier dokumentiert.
1212

13+
## [0.3.2] - 2026-09-14
14+
15+
### Visuelle Breakpoints im Zeilennummern-Gutter & Zuletzt geöffnete Dateien (Recent Files)
16+
17+
- `core/editor.py`:
18+
- Visueller Breakpoint-Indikator im Zeilennummern-Gutter (`LineNumberArea`): Antialiased roter Kreis (`#e51400`) mit dynamischer Skalierung und zentrierter Ausrichtung.
19+
- Breakpoint-Management auf `CodeEditor`: `toggle_breakpoint(line)`, `has_breakpoint(line)`, `get_breakpoints()`, `set_breakpoints(lines)`, `clear_breakpoints()`.
20+
- Signal `breakpointsChanged` informiert externe Listener über modifizierte Breakpoint-Mengen.
21+
- Interaktives Umschalten von Breakpoints per Mausklick in die Gutter-Fläche links neben den Zeilennummern mit Zeiger-Cursor (`PointingHandCursor`).
22+
- `ui/main_window.py`:
23+
- Menüpunkt *Ausführen -> Breakpoint umschalten* (`F9`) und *Ausführen -> Alle Breakpoints löschen* (`Ctrl+Shift+F9`) mit Statusleistenmeldungen und Tastenkürzeln.
24+
- Submenü *Datei -> Zuletzt geöffnete Dateien* mit persistenter Nachverfolgung (`recent_files` in Config-Settings), automatischer Bereinigung nicht mehr existierender Pfade, De-Duplizierung und Aktion "Verlauf leeren".
25+
- Automatisches Erfassen geöffneter und gespeicherter Dateien in `open_path` und `save_file`.
26+
- Robuste C++-Objekt-Lebenszyklus-Absicherung (`isValid`-Check & MainWindow-Parenting) gegen Shiboken-Wrapper-Invalidierung bei Menübaum-Traversierung.
27+
- `ui/shortcuts_dialog.py`:
28+
- Registrierung von `F9` (Breakpoint umschalten) und `Ctrl+Shift+F9` (Alle Breakpoints löschen) in der Tastenkürzel-Übersicht unter *Ausführen*.
29+
- `ui/command_palette.py`:
30+
- Schnellauswahl-Aktionen für Breakpoint-Umschaltung und -Bereinigung in den Quick Actions der Befehlspalette.
31+
- `tests/test_breakpoints_and_recent_files.py`:
32+
- 5 neue automatisierte Tests zur Überprüfung von Editor-Breakpoint-Logik, Gutter-Breiten, MainWindow-Menüaktionen und persistenter Recent-Files-Verwaltung.
33+
1334
## [0.3.1] - 2026-09-13
1435

1536
### Integrierte Aufgaben- und TODO-Seitenleiste (TodoPanel & TodoScanner Engine)

README.md

Lines changed: 2 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-273%20passed%20%7C%20100%25-brightgreen.svg)]()
9+
[![Tests](https://img.shields.io/badge/tests-285%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.3.0](https://img.shields.io/badge/version-0.3.0-green.svg)](CHANGELOG.md)
15+
[![Version: 0.3.2](https://img.shields.io/badge/version-0.3.2-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

README_de.md

Lines changed: 2 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
[![Plattform: 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-273%20passed%20%7C%20100%25-brightgreen.svg)]()
9+
[![Tests](https://img.shields.io/badge/tests-285%20passed%20%7C%20100%25-brightgreen.svg)]()
1010
[![Datenschutz: Zero-Egress](https://img.shields.io/badge/datenschutz-100%25%20local--first%20%7C%20zero--egress-success.svg)](SECURITY.md)
1111
[![Sicherheitsrichtlinie](https://img.shields.io/badge/sicherheit-zweisprachige%20policy-blue.svg)](SECURITY.md)
1212
[![Ökosystem: dev-bricks](https://img.shields.io/badge/ecosystem-dev--bricks-blue.svg)](https://github.com/dev-bricks)
1313
[![Dachverband: 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.3.0](https://img.shields.io/badge/version-0.3.0-green.svg)](CHANGELOG.md)
15+
[![Version: 0.3.2](https://img.shields.io/badge/version-0.3.2-green.svg)](CHANGELOG.md)
1616
[![llms.txt](https://img.shields.io/badge/llms.txt-verf%C3%BCgbar-green.svg)](llms.txt)
1717

1818
[English](README.md) | Deutsch

core/editor.py

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
class LineNumberArea(QWidget):
21-
"""Zeichnet Zeilennummern und Faltungs-Indikatoren für den CodeEditor"""
21+
"""Zeichnet Zeilennummern, Breakpoints und Faltungs-Indikatoren für den CodeEditor"""
2222
def __init__(self, editor):
2323
super().__init__(editor)
2424
self.codeEditor = editor
@@ -35,16 +35,21 @@ def mousePressEvent(self, event):
3535
block = self.codeEditor.firstVisibleBlock()
3636
top = int(self.codeEditor.blockBoundingGeometry(block).translated(self.codeEditor.contentOffset()).top())
3737
y = event.position().y()
38+
x = event.position().x()
39+
fold_x = self.width() - getattr(self.codeEditor, 'FOLD_AREA_WIDTH', 14)
3840
while block.isValid():
3941
if block.isVisible():
4042
h = int(self.codeEditor.blockBoundingRect(block).height())
4143
if top <= y < top + h:
4244
block_num = block.blockNumber()
43-
if self.codeEditor.is_line_foldable(block_num):
45+
if x >= fold_x - 4 and self.codeEditor.is_line_foldable(block_num):
4446
self.codeEditor.toggle_fold(block_num)
4547
event.accept()
4648
return
47-
break
49+
# Klick im Zeilennummer- oder Breakpoint-Bereich schaltet Breakpoint um
50+
self.codeEditor.toggle_breakpoint(block_num + 1)
51+
event.accept()
52+
return
4853
top += h
4954
block = block.next()
5055
super().mousePressEvent(event)
@@ -55,18 +60,19 @@ def mouseMoveEvent(self, event):
5560
y = event.position().y()
5661
x = event.position().x()
5762
fold_x = self.width() - getattr(self.codeEditor, 'FOLD_AREA_WIDTH', 14)
58-
is_hovering_fold = False
63+
bp_w = getattr(self.codeEditor, 'BREAKPOINT_AREA_WIDTH', 14)
64+
is_hovering_interactive = False
5965
while block.isValid():
6066
if block.isVisible():
6167
h = int(self.codeEditor.blockBoundingRect(block).height())
6268
if top <= y < top + h:
6369
block_num = block.blockNumber()
64-
if self.codeEditor.is_line_foldable(block_num) and x >= fold_x - 4:
65-
is_hovering_fold = True
70+
if (self.codeEditor.is_line_foldable(block_num) and x >= fold_x - 4) or (x <= bp_w + 4):
71+
is_hovering_interactive = True
6672
break
6773
top += h
6874
block = block.next()
69-
if is_hovering_fold:
75+
if is_hovering_interactive:
7076
self.setCursor(Qt.CursorShape.PointingHandCursor)
7177
else:
7278
self.setCursor(Qt.CursorShape.ArrowCursor)
@@ -235,6 +241,7 @@ class CodeEditor(QPlainTextEdit):
235241
completionRequested = Signal(int, int, str) # LSP: Zeile, Spalte, Prefix (0-basiert)
236242
definitionRequested = Signal(int, int, str) # LSP/Fallback: Zeile, Spalte, Symbol (0-basiert)
237243
referencesRequested = Signal(int, int, str) # LSP/Fallback: Zeile, Spalte, Symbol (0-basiert)
244+
breakpointsChanged = Signal(object) # List[int] mit 1-basierten Zeilennummern
238245
modificationChanged = Signal(bool)
239246
focusReceived = Signal()
240247

@@ -253,6 +260,9 @@ def __init__(self, parent=None):
253260
self.bracket_matching_enabled = True
254261
self.linter_errors: List[Dict] = []
255262

263+
self.BREAKPOINT_AREA_WIDTH = 14
264+
self._breakpoints: set[int] = set()
265+
256266
self.lineNumberArea = LineNumberArea(self)
257267
self.minimap = Minimap(self)
258268
self._minimap_visible = True
@@ -894,7 +904,8 @@ def lineNumberAreaWidth(self):
894904
fm = self.fontMetrics()
895905
char_width = fm.horizontalAdvance('9') if hasattr(fm, 'horizontalAdvance') else fm.width('9')
896906
fold_w = getattr(self, 'FOLD_AREA_WIDTH', 14)
897-
return 16 + char_width * digits + fold_w
907+
bp_w = getattr(self, 'BREAKPOINT_AREA_WIDTH', 14)
908+
return bp_w + char_width * digits + fold_w + 14
898909

899910
def updateLineNumberAreaWidth(self, _):
900911
right_margin = self.minimap.width() if self._minimap_visible else 0
@@ -934,12 +945,25 @@ def lineNumberAreaPaintEvent(self, event):
934945
top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
935946
bottom = top + int(self.blockBoundingRect(block).height())
936947
fold_width = getattr(self, 'FOLD_AREA_WIDTH', 14)
948+
bp_width = getattr(self, 'BREAKPOINT_AREA_WIDTH', 14)
937949
area_width = self.lineNumberArea.width()
938950
font_h = self.fontMetrics().height()
939951

940952
while block.isValid() and top <= event.rect().bottom():
941953
if block.isVisible() and bottom >= event.rect().top():
942954
line_num = blockNumber + 1
955+
956+
# Breakpoint-Indikator zeichnen (roter Kreis im Gutter)
957+
if hasattr(self, '_breakpoints') and line_num in self._breakpoints:
958+
painter.save()
959+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
960+
painter.setPen(Qt.PenStyle.NoPen)
961+
painter.setBrush(QColor(229, 20, 0))
962+
bp_cx = (bp_width // 2) + 1
963+
bp_cy = top + (font_h // 2)
964+
painter.drawEllipse(QPointF(bp_cx, bp_cy), 4.5, 4.5)
965+
painter.restore()
966+
943967
has_error = any(e['line'] == line_num and e.get('severity') == 'error'
944968
for e in self.linter_errors)
945969
has_warning = any(e['line'] == line_num and e.get('severity') == 'warning'
@@ -951,8 +975,9 @@ def lineNumberAreaPaintEvent(self, event):
951975
else:
952976
painter.setPen(QColor(100, 100, 100))
953977

954-
num_rect_w = max(0, area_width - fold_width - 4)
955-
painter.drawText(0, top, num_rect_w,
978+
num_x = bp_width + 4
979+
num_rect_w = max(0, area_width - fold_width - bp_width - 8)
980+
painter.drawText(num_x, top, num_rect_w,
956981
font_h, Qt.AlignmentFlag.AlignRight, str(line_num))
957982

958983
if hasattr(self, 'folding_manager') and self.folding_manager.is_line_foldable(blockNumber):
@@ -988,6 +1013,50 @@ def lineNumberAreaPaintEvent(self, event):
9881013
bottom = top + int(self.blockBoundingRect(block).height())
9891014
blockNumber += 1
9901015

1016+
# ---- Breakpoint Management ----
1017+
1018+
def toggle_breakpoint(self, line: Optional[int] = None) -> bool:
1019+
"""Schaltet einen Breakpoint auf der angegebenen Zeile (1-basiert) oder der Cursorzeile um."""
1020+
if line is None:
1021+
line = self.textCursor().blockNumber() + 1
1022+
line = int(line)
1023+
if line < 1:
1024+
return False
1025+
1026+
if line in self._breakpoints:
1027+
self._breakpoints.remove(line)
1028+
active = False
1029+
else:
1030+
self._breakpoints.add(line)
1031+
active = True
1032+
1033+
if hasattr(self, 'lineNumberArea'):
1034+
self.lineNumberArea.update()
1035+
self.breakpointsChanged.emit(self.get_breakpoints())
1036+
return active
1037+
1038+
def has_breakpoint(self, line: int) -> bool:
1039+
"""Prüft, ob auf der Zeile (1-basiert) ein Breakpoint gesetzt ist."""
1040+
return int(line) in self._breakpoints
1041+
1042+
def get_breakpoints(self) -> List[int]:
1043+
"""Gibt eine sortierte Liste aller gesetzten Breakpoints (1-basiert) zurück."""
1044+
return sorted(self._breakpoints)
1045+
1046+
def set_breakpoints(self, lines: List[int]):
1047+
"""Setzt die Liste aller Breakpoints (1-basiert)."""
1048+
self._breakpoints = {int(line_num) for line_num in lines if int(line_num) >= 1}
1049+
if hasattr(self, 'lineNumberArea'):
1050+
self.lineNumberArea.update()
1051+
self.breakpointsChanged.emit(self.get_breakpoints())
1052+
1053+
def clear_breakpoints(self):
1054+
"""Entfernt alle gesetzten Breakpoints in diesem Editor."""
1055+
self._breakpoints.clear()
1056+
if hasattr(self, 'lineNumberArea'):
1057+
self.lineNumberArea.update()
1058+
self.breakpointsChanged.emit([])
1059+
9911060
# ---- Code Folding ----
9921061

9931062
def _schedule_fold_update(self):

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "CodeBox"
7-
version = "0.3.1"
7+
version = "0.3.2"
88
description = "Lightweight local PySide6 desktop code editor with LSP diagnostics, tabs, project tree, and integrated terminal"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -71,7 +71,7 @@ Security = "https://github.com/dev-bricks/CodeBox/blob/main/SECURITY.md"
7171
minversion = "7.0"
7272
testpaths = ["tests"]
7373
pythonpath = ["."]
74-
addopts = "-ra"
74+
addopts = "-ra -p no:flask"
7575

7676
[tool.ruff]
7777
line-length = 100

0 commit comments

Comments
 (0)