Skip to content

Commit a4f3e21

Browse files
Lukas Geigerclaude
andcommitted
fix: bugsweep fixes [bugsweep-auto]
20 bugs fixed across PythonBox_v8.py and translator.py: - Bug #1-8 (prior session) - Bug #9: tree_drop - UnicodeDecodeError bei nicht-UTF-8-Dateien - Bug #10: optimize_imports_action - setPlainText zerstört Undo-History; nutze insertText/beginEditBlock - Bug #11: insert_completion - einfügen wenn extra==0 (guard: elif extra > 0) - Bug #12: translator._is_german - Einzelzeichen-Check auf 'aeoeue...' flaggte jeden Text - Bug #13: closeEvent - fehlender Speicher-Dialog bei ungespeicherten Tabs - Bug #14: LinterRunner._run_flake8/_run_pylint - ValueError/IndexError nur 1 Zeile übersprungen statt alle - Bug #15: get_modified_lines - Diff-Dateiheader rückte Zeilenzähler fälschlicherweise vor (in_hunk-Flag) - Bug #16: FoldingArea.paintEvent - kein isValid()-Check nach block.next() - Bug #17: lineNumberAreaPaintEvent - kein isValid()-Check nach block.next() - Bug #18: _update_minimap - alte Minimap nicht aus Layout entfernt vor deleteLater - Bug #19: on_editor_changed - Signal-Slot doppelt verbunden bei Tab-Wechsel - Bug #20: ImportOptimizer.organize_imports - ast.walk statt tree.body (hob Inline-Imports hoch) 37 Regressionstests bestehen; 15 schlagen nur wegen fehlendem PySide6 im Test-Env fehl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0850091 commit a4f3e21

3 files changed

Lines changed: 831 additions & 53 deletions

File tree

PythonBox_v8.py

Lines changed: 140 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,9 @@ def insert_completion(self, completion: str):
386386
tc.movePosition(QTextCursor.MoveOperation.StartOfWord, QTextCursor.MoveMode.KeepAnchor)
387387
tc.removeSelectedText()
388388
tc.insertText(PYTHON_SNIPPETS[completion])
389-
else:
389+
elif extra > 0:
390390
tc.insertText(completion[-extra:])
391-
391+
392392
self.setTextCursor(tc)
393393

394394
def text_under_cursor(self) -> str:
@@ -545,21 +545,23 @@ def _build_string_comment_mask(text: str) -> list:
545545
mask[j] = True
546546
j += 1
547547
i = j
548-
# Single/double quoted string
548+
# Single/double quoted string (single-line only — no newline crossing)
549549
elif text[i] in ('"', "'"):
550550
q = text[i]
551551
mask[i] = True
552552
i += 1
553-
while i < n and text[i] != q:
553+
while i < n and text[i] != q and text[i] != '\n':
554554
if text[i] == '\\':
555555
mask[i] = True
556556
i += 1 # escape char
557557
if i < n:
558558
mask[i] = True
559559
i += 1
560-
if i < n:
560+
if i < n and text[i] == q:
561561
mask[i] = True # closing quote
562-
i += 1
562+
i += 1
563+
# If we hit a newline the string was unterminated on this line;
564+
# do NOT advance past the newline so it is handled normally.
563565
else:
564566
i += 1
565567
return mask
@@ -694,7 +696,8 @@ def lineNumberAreaPaintEvent(self, event):
694696

695697
block = block.next()
696698
top = bottom
697-
bottom = top + int(self.blockBoundingRect(block).height())
699+
if block.isValid():
700+
bottom = top + int(self.blockBoundingRect(block).height())
698701
blockNumber += 1
699702

700703
def lineNumberAreaMousePress(self, event):
@@ -1093,7 +1096,8 @@ def paintEvent(self, event):
10931096

10941097
block = block.next()
10951098
top = bottom
1096-
bottom = top + int(self.editor.blockBoundingRect(block).height())
1099+
if block.isValid():
1100+
bottom = top + int(self.editor.blockBoundingRect(block).height())
10971101

10981102
def mousePressEvent(self, event):
10991103
"""Toggle Folding beim Klick"""
@@ -1212,14 +1216,17 @@ def _run_flake8(self, file_path: str) -> List[Dict]:
12121216
if ':' in line:
12131217
parts = line.split(':', 3)
12141218
if len(parts) >= 4:
1215-
results.append({
1216-
'line': int(parts[0]),
1217-
'column': int(parts[1]),
1218-
'code': parts[2],
1219-
'message': parts[3],
1220-
'severity': 'error' if parts[2].startswith('E') else 'warning'
1221-
})
1222-
except Exception as e:
1219+
try:
1220+
results.append({
1221+
'line': int(parts[0]),
1222+
'column': int(parts[1]),
1223+
'code': parts[2],
1224+
'message': parts[3],
1225+
'severity': 'error' if parts[2].startswith('E') else 'warning'
1226+
})
1227+
except (ValueError, IndexError):
1228+
pass
1229+
except Exception:
12231230
pass
12241231

12251232
return results
@@ -1238,14 +1245,17 @@ def _run_pylint(self, file_path: str) -> List[Dict]:
12381245
if ':' in line and not line.startswith('*'):
12391246
parts = line.split(':', 3)
12401247
if len(parts) >= 4:
1241-
results.append({
1242-
'line': int(parts[0]),
1243-
'column': int(parts[1]),
1244-
'code': parts[2],
1245-
'message': parts[3],
1246-
'severity': 'error' if parts[2].startswith('E') else 'warning'
1247-
})
1248-
except Exception as e:
1248+
try:
1249+
results.append({
1250+
'line': int(parts[0]),
1251+
'column': int(parts[1]),
1252+
'code': parts[2],
1253+
'message': parts[3],
1254+
'severity': 'error' if parts[2].startswith('E') else 'warning'
1255+
})
1256+
except (ValueError, IndexError):
1257+
pass
1258+
except Exception:
12491259
pass
12501260

12511261
return results
@@ -1388,6 +1398,7 @@ def get_modified_lines(self, file_path: str) -> Tuple[set, set, set]:
13881398
return added, modified, deleted
13891399

13901400
current_line = 0
1401+
in_hunk = False
13911402
pending_deletion = False
13921403
for line in diff.split('\n'):
13931404
if line.startswith('@@'):
@@ -1396,6 +1407,7 @@ def get_modified_lines(self, file_path: str) -> Tuple[set, set, set]:
13961407
if match:
13971408
current_line = int(match.group(1)) - 1
13981409
pending_deletion = False
1410+
in_hunk = True
13991411
elif line.startswith('+') and not line.startswith('+++'):
14001412
current_line += 1
14011413
if pending_deletion:
@@ -1406,7 +1418,8 @@ def get_modified_lines(self, file_path: str) -> Tuple[set, set, set]:
14061418
elif line.startswith('-') and not line.startswith('---'):
14071419
deleted.add(current_line)
14081420
pending_deletion = True
1409-
else:
1421+
elif in_hunk:
1422+
# Only context lines (starting with ' ') inside a hunk advance the counter
14101423
current_line += 1
14111424
pending_deletion = False
14121425

@@ -2170,9 +2183,16 @@ def replace_current(self):
21702183
if not self.editor:
21712184
return
21722185
cursor = self.editor.textCursor()
2173-
if cursor.hasSelection() and cursor.selectedText().lower() == self.search_input.text().lower():
2174-
cursor.insertText(self.replace_input.text())
2175-
self.on_search_changed()
2186+
if cursor.hasSelection():
2187+
selected = cursor.selectedText()
2188+
search = self.search_input.text()
2189+
if self.case_check.isChecked():
2190+
match = selected == search
2191+
else:
2192+
match = selected.lower() == search.lower()
2193+
if match:
2194+
cursor.insertText(self.replace_input.text())
2195+
self.on_search_changed()
21762196
self.find_next()
21772197

21782198
def replace_all(self):
@@ -2191,7 +2211,8 @@ def replace_all(self):
21912211
if new_text != text:
21922212
cursor = self.editor.textCursor()
21932213
cursor.beginEditBlock()
2194-
self.editor.setPlainText(new_text)
2214+
cursor.select(QTextCursor.SelectionType.Document)
2215+
cursor.insertText(new_text)
21952216
cursor.endEditBlock()
21962217
self.on_search_changed()
21972218

@@ -2292,7 +2313,9 @@ def process_error(self, error):
22922313
def stop_process(self):
22932314
if self.process:
22942315
self.process.kill()
2316+
self.process.waitForFinished(1000)
22952317
self.append_output("\n⏹ Prozess abgebrochen.\n")
2318+
self.btn_stop.setEnabled(False)
22962319

22972320
def clear_output(self):
22982321
self.output_text.clear()
@@ -2367,26 +2390,38 @@ def new_tab(self, file_path: Optional[str] = None, content: str = "") -> int:
23672390
tab_data = EditorTab(editor, highlighter, file_path)
23682391
self.tabs[index] = tab_data
23692392

2370-
# Verbinde Modifikations-Signal
2393+
# Verbinde Modifikations-Signal — nutze editor-Identität statt Index,
2394+
# damit das Signal nach Tab-Reindizierung (close_tab) noch korrekt funktioniert.
23712395
editor.document().modificationChanged.connect(
2372-
lambda modified, idx=index: self._on_modification_changed(idx, modified)
2396+
lambda modified, ed=editor: self._on_modification_changed_by_editor(ed, modified)
23732397
)
23742398

23752399
self.tab_widget.setCurrentIndex(index)
23762400
return index
23772401

2402+
def _on_modification_changed_by_editor(self, editor: 'CodeEditor', modified: bool):
2403+
"""Aktualisiert Tab-Titel bei Änderungen — sucht Tab per Editor-Objekt.
2404+
2405+
Robust gegen Tab-Reindizierung: Statt dem zum Erstellungszeitpunkt
2406+
gespeicherten Index wird das Editor-Objekt als stabile Identität genutzt.
2407+
"""
2408+
for index, tab in self.tabs.items():
2409+
if tab.editor is editor:
2410+
self._on_modification_changed(index, modified)
2411+
return
2412+
23782413
def _on_modification_changed(self, index: int, modified: bool):
23792414
"""Aktualisiert Tab-Titel bei Änderungen"""
23802415
if index not in self.tabs:
23812416
return
2382-
2417+
23832418
tab = self.tabs[index]
23842419
tab.is_modified = modified
2385-
2420+
23862421
name = Path(tab.file_path).name if tab.file_path else f"Unbenannt"
23872422
if modified:
23882423
name = f"● {name}"
2389-
2424+
23902425
self.tab_widget.setTabText(index, name)
23912426
self.fileModified.emit(name, modified)
23922427

@@ -2518,14 +2553,17 @@ def organize_imports(code):
25182553
other_lines = code.splitlines()
25192554
import_linenos = set()
25202555

2521-
for node in ast.walk(tree):
2556+
# Only process TOP-LEVEL import nodes (direct children of the module body).
2557+
# Using ast-walk would also pick up imports inside functions/classes/if-blocks,
2558+
# incorrectly moving conditional or lazy imports to module level.
2559+
for node in tree.body:
25222560
if isinstance(node, ast.Import):
25232561
for alias in node.names:
25242562
name = alias.name
25252563
asname = f" as {alias.asname}" if alias.asname else ""
25262564
imports.append(f"import {name}{asname}")
25272565
import_linenos.update(range(node.lineno - 1, node.end_lineno))
2528-
2566+
25292567
elif isinstance(node, ast.ImportFrom):
25302568
module = node.module if node.module else ""
25312569
names = []
@@ -2541,14 +2579,27 @@ def organize_imports(code):
25412579

25422580
new_body = [line for i, line in enumerate(other_lines) if i not in import_linenos]
25432581

2544-
header = imports + from_imports
2545-
if header and new_body:
2546-
final_code = "\n".join(header) + "\n\n" + "\n".join(new_body)
2547-
elif header:
2548-
final_code = "\n".join(header)
2549-
else:
2550-
final_code = "\n".join(new_body)
2582+
# Preserve leading shebang and encoding declaration (must stay at top of file).
2583+
# PEP 263: encoding declaration must be in line 1 or 2.
2584+
# Shebang (#!) must be the very first line if present.
2585+
leading_lines = []
2586+
while new_body:
2587+
stripped = new_body[0].strip()
2588+
if stripped.startswith('#!') or re.match(r'^#.*coding[:=]', stripped):
2589+
leading_lines.append(new_body.pop(0))
2590+
else:
2591+
break
25512592

2593+
header = imports + from_imports
2594+
parts = []
2595+
if leading_lines:
2596+
parts.append("\n".join(leading_lines))
2597+
if header:
2598+
parts.append("\n".join(header))
2599+
if new_body:
2600+
parts.append("\n".join(new_body))
2601+
2602+
final_code = "\n\n".join(p for p in parts if p)
25522603
final_code = re.sub(r'\n{3,}', '\n\n', final_code)
25532604
return final_code, "Imports optimiert."
25542605

@@ -3058,9 +3109,6 @@ def setup_ui(self):
30583109
self.addDockWidget(Qt.BottomDockWidgetArea, self.linter_dock)
30593110
self.linter_dock.hide()
30603111

3061-
# --- MINIMAP (NEU v7!) ---
3062-
self.minimap = None # Wird bei Bedarf erstellt
3063-
30643112
# --- STATUS BAR ---
30653113
self.status_bar = QStatusBar()
30663114
self.setStatusBar(self.status_bar)
@@ -3086,6 +3134,11 @@ def on_editor_changed(self, editor: CodeEditor):
30863134
"""Wird aufgerufen wenn der aktive Editor wechselt"""
30873135
if editor:
30883136
self.search_bar.set_editor(editor)
3137+
# Disconnect first to avoid duplicate connections when switching back
3138+
try:
3139+
editor.cursorPositionInfo.disconnect(self.update_cursor_position)
3140+
except RuntimeError:
3141+
pass
30893142
editor.cursorPositionInfo.connect(self.update_cursor_position)
30903143

30913144
# NEU v7: Git-Status aktualisieren (nur wenn git_label existiert)
@@ -3148,6 +3201,22 @@ def _restore_window_state(self):
31483201
self.restoreGeometry(geometry)
31493202

31503203
def closeEvent(self, event):
3204+
# Check for unsaved tabs before closing
3205+
for index, tab in list(self.tab_editor.tabs.items()):
3206+
if tab.is_modified:
3207+
name = Path(tab.file_path).name if tab.file_path else "Unbenannt"
3208+
result = QMessageBox.question(
3209+
self, "Speichern?",
3210+
f"'{name}' wurde geändert. Speichern?",
3211+
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
3212+
)
3213+
if result == QMessageBox.Cancel:
3214+
event.ignore()
3215+
return
3216+
elif result == QMessageBox.Save:
3217+
if not self.tab_editor.save_tab(index):
3218+
event.ignore()
3219+
return
31513220
self.settings.setValue("geometry", self.saveGeometry())
31523221
event.accept()
31533222

@@ -3323,23 +3392,32 @@ def toggle_minimap(self, checked: bool):
33233392
editor = self.tab_editor.current_editor()
33243393
if editor:
33253394
self._update_minimap(editor)
3395+
else:
3396+
# Minimap-Objekt freigeben damit keine unnötigen Signal-Updates mehr laufen.
3397+
if self.minimap:
3398+
self.minimap.deleteLater()
3399+
self.minimap = None
33263400
self.status_bar.showMessage(f"Minimap {'aktiviert' if checked else 'deaktiviert'}", 2000)
33273401

33283402
def _update_minimap(self, editor: CodeEditor):
33293403
"""Aktualisiert/Erstellt Minimap für Editor"""
33303404
if not self.minimap_action.isChecked():
33313405
return
3332-
3333-
# Alte Minimap entfernen
3406+
3407+
# Alte Minimap aus Layout entfernen und freigeben
33343408
if self.minimap:
3409+
layout = self.minimap_container.layout()
3410+
if layout:
3411+
layout.removeWidget(self.minimap)
33353412
self.minimap.deleteLater()
3336-
3413+
self.minimap = None
3414+
33373415
# Neue Minimap erstellen
33383416
layout = self.minimap_container.layout()
33393417
if not layout:
33403418
layout = QVBoxLayout(self.minimap_container)
33413419
layout.setContentsMargins(0, 0, 0, 0)
3342-
3420+
33433421
self.minimap = Minimap(editor, self.minimap_container)
33443422
layout.addWidget(self.minimap)
33453423

@@ -3821,7 +3899,13 @@ def tree_drop(self, event):
38213899
for url in event.mimeData().urls():
38223900
path = Path(url.toLocalFile())
38233901
if path.is_file() and path.suffix == '.py':
3824-
content = path.read_text(encoding='utf-8')
3902+
try:
3903+
content = path.read_text(encoding='utf-8')
3904+
except (UnicodeDecodeError, OSError):
3905+
try:
3906+
content = path.read_text(encoding='latin-1')
3907+
except OSError:
3908+
continue
38253909
self.lib_manager.save_snippet("Importiert", path.stem, content)
38263910
self.load_library_tree()
38273911
elif event.mimeData().hasText():
@@ -3855,7 +3939,11 @@ def optimize_imports_action(self):
38553939
code = editor.toPlainText()
38563940
new_code, msg = ImportOptimizer.organize_imports(code)
38573941
if new_code:
3858-
editor.setPlainText(new_code)
3942+
cursor = editor.textCursor()
3943+
cursor.beginEditBlock()
3944+
cursor.select(QTextCursor.SelectionType.Document)
3945+
cursor.insertText(new_code)
3946+
cursor.endEditBlock()
38593947
self.status_bar.showMessage(msg, 3000)
38603948
else:
38613949
QMessageBox.warning(self, "Fehler", msg)

0 commit comments

Comments
 (0)