22
33import re
44from PyQt6 .QtWidgets import QPlainTextEdit
5- from PyQt6 .QtGui import QSyntaxHighlighter , QTextCharFormat , QColor , QFont
5+ from PyQt6 .QtGui import QSyntaxHighlighter , QTextCharFormat , QColor , QFont , QTextCursor
66from PyQt6 .QtCore import Qt , QRegularExpression , QTimer
77from typing import Optional
88
@@ -117,11 +117,16 @@ def __init__(self, parent=None):
117117 self .file_path : Optional [str ] = None
118118 self .is_modified = False
119119
120+ # Indentation settings (defaults, will be updated by app)
121+ self .tab_size = 4
122+ self .use_spaces = True
123+ self .auto_indent = True
124+
120125 font = QFont ("Consolas" , 10 )
121126 font .setStyleHint (QFont .StyleHint .Monospace )
122127 self .setFont (font )
123128
124- self .setTabStopDistance ( 40 )
129+ self ._update_tab_stop_distance ( )
125130
126131 # Use fast syntax highlighter with Trie
127132 self .highlighter = FastSyntaxHighlighter (self .document ())
@@ -133,6 +138,221 @@ def __init__(self, parent=None):
133138
134139 self .textChanged .connect (self ._on_text_changed )
135140
141+ def _update_tab_stop_distance (self ):
142+ """Update tab stop distance based on tab size setting."""
143+ # Get the width of a space character
144+ font_metrics = self .fontMetrics ()
145+ space_width = font_metrics .horizontalAdvance (' ' )
146+ self .setTabStopDistance (space_width * self .tab_size )
147+
148+ def update_indentation_settings (self , tab_size : int , use_spaces : bool , auto_indent : bool ):
149+ """Update indentation settings from app settings."""
150+ self .tab_size = tab_size
151+ self .use_spaces = use_spaces
152+ self .auto_indent = auto_indent
153+ self ._update_tab_stop_distance ()
154+
155+ def keyPressEvent (self , event ):
156+ """Handle key press events for proper indentation."""
157+ # Handle Tab key
158+ if event .key () == Qt .Key .Key_Tab :
159+ self ._handle_tab ()
160+ return
161+
162+ # Handle Shift+Tab (unindent)
163+ if event .key () == Qt .Key .Key_Backtab :
164+ self ._handle_backtab ()
165+ return
166+
167+ # Handle Enter/Return for auto-indent
168+ if event .key () in (Qt .Key .Key_Return , Qt .Key .Key_Enter ):
169+ if self .auto_indent :
170+ self ._handle_return_with_indent ()
171+ return
172+
173+ # Default behavior for other keys
174+ super ().keyPressEvent (event )
175+
176+ def _handle_tab (self ):
177+ """Handle Tab key press - insert spaces or tab based on settings."""
178+ cursor = self .textCursor ()
179+
180+ # Check if there's a selection
181+ if cursor .hasSelection ():
182+ # Indent selected lines
183+ self ._indent_selection (cursor )
184+ else :
185+ # Insert tab or spaces at cursor
186+ if self .use_spaces :
187+ # Calculate spaces needed to reach next tab stop
188+ column = cursor .columnNumber ()
189+ spaces_to_insert = self .tab_size - (column % self .tab_size )
190+ cursor .insertText (' ' * spaces_to_insert )
191+ else :
192+ cursor .insertText ('\t ' )
193+
194+ def _handle_backtab (self ):
195+ """Handle Shift+Tab - unindent."""
196+ cursor = self .textCursor ()
197+
198+ if cursor .hasSelection ():
199+ # Unindent selected lines
200+ self ._unindent_selection (cursor )
201+ else :
202+ # Remove indentation at cursor
203+ self ._unindent_line (cursor )
204+
205+ def _indent_selection (self , cursor ):
206+ """Indent all lines in the selection."""
207+ # Get selection start and end
208+ start = cursor .selectionStart ()
209+ end = cursor .selectionEnd ()
210+
211+ # Move to start of selection
212+ cursor .setPosition (start )
213+ cursor .movePosition (QTextCursor .MoveOperation .StartOfLine )
214+
215+ # Get the block at start and end
216+ start_block = cursor .block ()
217+ cursor .setPosition (end )
218+ end_block = cursor .block ()
219+
220+ # Prepare indentation string
221+ indent_str = ' ' * self .tab_size if self .use_spaces else '\t '
222+
223+ # Start edit block for undo/redo
224+ cursor .beginEditBlock ()
225+
226+ # Iterate through all blocks in selection
227+ cursor .setPosition (start_block .position ())
228+ while True :
229+ cursor .movePosition (QTextCursor .MoveOperation .StartOfLine )
230+ cursor .insertText (indent_str )
231+
232+ if cursor .block () == end_block :
233+ break
234+
235+ if not cursor .movePosition (QTextCursor .MoveOperation .NextBlock ):
236+ break
237+
238+ cursor .endEditBlock ()
239+
240+ def _unindent_selection (self , cursor ):
241+ """Unindent all lines in the selection."""
242+ # Get selection start and end
243+ start = cursor .selectionStart ()
244+ end = cursor .selectionEnd ()
245+
246+ # Move to start of selection
247+ cursor .setPosition (start )
248+ cursor .movePosition (QTextCursor .MoveOperation .StartOfLine )
249+
250+ # Get the block at start and end
251+ start_block = cursor .block ()
252+ cursor .setPosition (end )
253+ end_block = cursor .block ()
254+
255+ # Start edit block for undo/redo
256+ cursor .beginEditBlock ()
257+
258+ # Iterate through all blocks in selection
259+ cursor .setPosition (start_block .position ())
260+ while True :
261+ cursor .movePosition (QTextCursor .MoveOperation .StartOfLine )
262+ self ._remove_indentation_from_line (cursor )
263+
264+ if cursor .block () == end_block :
265+ break
266+
267+ if not cursor .movePosition (QTextCursor .MoveOperation .NextBlock ):
268+ break
269+
270+ cursor .endEditBlock ()
271+
272+ def _unindent_line (self , cursor ):
273+ """Unindent the current line."""
274+ cursor .beginEditBlock ()
275+ cursor .movePosition (QTextCursor .MoveOperation .StartOfLine )
276+ self ._remove_indentation_from_line (cursor )
277+ cursor .endEditBlock ()
278+
279+ def _remove_indentation_from_line (self , cursor ):
280+ """Remove one level of indentation from the current line."""
281+ line_text = cursor .block ().text ()
282+
283+ if not line_text :
284+ return
285+
286+ # Count leading whitespace
287+ leading_spaces = len (line_text ) - len (line_text .lstrip (' \t ' ))
288+
289+ if leading_spaces == 0 :
290+ return
291+
292+ # Determine how much to remove
293+ if self .use_spaces :
294+ # Remove up to tab_size spaces
295+ to_remove = min (self .tab_size , leading_spaces )
296+
297+ # Count actual spaces (not tabs)
298+ space_count = 0
299+ for char in line_text [:leading_spaces ]:
300+ if char == ' ' :
301+ space_count += 1
302+ if space_count >= to_remove :
303+ break
304+ elif char == '\t ' :
305+ # Treat tab as one indentation level
306+ to_remove = 1
307+ break
308+
309+ to_remove = min (to_remove , space_count ) if space_count > 0 else (1 if '\t ' in line_text [:leading_spaces ] else 0 )
310+ else :
311+ # Remove one tab or up to tab_size spaces
312+ if line_text [0 ] == '\t ' :
313+ to_remove = 1
314+ else :
315+ to_remove = min (self .tab_size , leading_spaces )
316+
317+ # Remove the indentation
318+ if to_remove > 0 :
319+ cursor .movePosition (QTextCursor .MoveOperation .Right , QTextCursor .MoveMode .KeepAnchor , to_remove )
320+ cursor .removeSelectedText ()
321+
322+ def _handle_return_with_indent (self ):
323+ """Handle Return key with auto-indentation."""
324+ cursor = self .textCursor ()
325+
326+ # Get current line
327+ current_line = cursor .block ().text ()
328+
329+ # Calculate indentation of current line
330+ indent = self ._get_line_indentation (current_line )
331+
332+ # Check if line ends with opening brace - add extra indent
333+ stripped = current_line .rstrip ()
334+ extra_indent = ''
335+ if stripped .endswith ('{' ) or stripped .endswith (':' ):
336+ extra_indent = ' ' * self .tab_size if self .use_spaces else '\t '
337+
338+ # Insert newline and indentation
339+ cursor .insertText ('\n ' + indent + extra_indent )
340+
341+ def _get_line_indentation (self , line : str ) -> str :
342+ """Get the indentation string from a line."""
343+ indent = ''
344+ for char in line :
345+ if char in ' \t ' :
346+ indent += char
347+ else :
348+ break
349+
350+ # Convert tabs to spaces if using spaces
351+ if self .use_spaces and '\t ' in indent :
352+ indent = indent .replace ('\t ' , ' ' * self .tab_size )
353+
354+ return indent
355+
136356 def _schedule_highlight (self ):
137357 """Schedule delayed highlight to reduce CPU during rapid typing."""
138358 self .highlight_timer .stop ()
0 commit comments