@@ -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