Skip to content

Commit 4b43282

Browse files
committed
fix: toggle and indentation in editor
1 parent a2e549b commit 4b43282

File tree

4 files changed

+295
-2
lines changed

4 files changed

+295
-2
lines changed

src/cpplab/app.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,13 @@ def open_file_in_editor(self, file_path: str):
750750
editor.load_file(abs_path)
751751
editor.textChanged.connect(lambda: self._on_editor_modified(editor))
752752

753+
# Apply indentation settings
754+
editor.update_indentation_settings(
755+
self.settings.tab_size,
756+
self.settings.use_spaces,
757+
self.settings.auto_indent
758+
)
759+
753760
tab_name = Path(abs_path).name
754761
idx = self.editorTabWidget.addTab(editor, tab_name)
755762
self.editorTabWidget.setCurrentIndex(idx)
@@ -1184,6 +1191,14 @@ def on_problem_activated(self, row: int, column: int) -> None:
11841191
try:
11851192
editor = CodeEditor()
11861193
editor.load_file(str(file_path))
1194+
1195+
# Apply indentation settings
1196+
editor.update_indentation_settings(
1197+
self.settings.tab_size,
1198+
self.settings.use_spaces,
1199+
self.settings.auto_indent
1200+
)
1201+
11871202
tab_name = file_path.name
11881203
self.editorTabWidget.addTab(editor, tab_name)
11891204
self.editorTabWidget.setCurrentWidget(editor)
@@ -1471,6 +1486,14 @@ def apply_settings(self):
14711486
# Classic theme - use default styling
14721487
self.setStyleSheet("")
14731488

1489+
# Apply indentation settings to all open editors
1490+
for editor in self.open_editors.values():
1491+
editor.update_indentation_settings(
1492+
self.settings.tab_size,
1493+
self.settings.use_spaces,
1494+
self.settings.auto_indent
1495+
)
1496+
14741497
# Store build-related settings for use in build operations
14751498
# (accessed later in build methods)
14761499

src/cpplab/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class AppSettings:
1414
build_output_bold: bool = True
1515
incremental_builds: bool = True
1616
show_build_elapsed: bool = True
17+
# Editor settings
18+
tab_size: int = 4 # Number of spaces per tab
19+
use_spaces: bool = True # Use spaces instead of tabs
20+
auto_indent: bool = True # Automatically indent new lines
1721

1822

1923
def _get_settings_path() -> Path:

src/cpplab/settings_dialog.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def _create_ui(self):
3838
build_tab = self._create_build_tab()
3939
self.tabs.addTab(build_tab, "Build")
4040

41+
# Editor tab
42+
editor_tab = self._create_editor_tab()
43+
self.tabs.addTab(editor_tab, "Editor")
44+
4145
# Button box
4246
button_box = QDialogButtonBox(
4347
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
@@ -105,6 +109,40 @@ def _create_build_tab(self) -> QWidget:
105109
layout.addStretch()
106110
return widget
107111

112+
def _create_editor_tab(self) -> QWidget:
113+
"""Create editor settings tab."""
114+
widget = QWidget()
115+
layout = QVBoxLayout(widget)
116+
117+
# Indentation group
118+
indent_group = QGroupBox("Indentation")
119+
indent_layout = QFormLayout()
120+
121+
self.tab_size_spin = QSpinBox()
122+
self.tab_size_spin.setRange(1, 8)
123+
self.tab_size_spin.setSuffix(" spaces")
124+
self.tab_size_spin.setToolTip("Number of spaces per tab/indent level")
125+
indent_layout.addRow("Tab size:", self.tab_size_spin)
126+
127+
self.use_spaces_check = QCheckBox("Insert spaces instead of tabs")
128+
self.use_spaces_check.setToolTip(
129+
"When enabled, pressing Tab will insert spaces. "
130+
"When disabled, it will insert a tab character."
131+
)
132+
indent_layout.addRow("", self.use_spaces_check)
133+
134+
self.auto_indent_check = QCheckBox("Auto-indent new lines")
135+
self.auto_indent_check.setToolTip(
136+
"Automatically match the indentation of the previous line"
137+
)
138+
indent_layout.addRow("", self.auto_indent_check)
139+
140+
indent_group.setLayout(indent_layout)
141+
layout.addWidget(indent_group)
142+
143+
layout.addStretch()
144+
return widget
145+
108146
def _load_values(self):
109147
"""Load current settings into UI controls."""
110148
# Appearance
@@ -118,6 +156,11 @@ def _load_values(self):
118156
# Build
119157
self.incremental_check.setChecked(self.settings.incremental_builds)
120158
self.elapsed_check.setChecked(self.settings.show_build_elapsed)
159+
160+
# Editor
161+
self.tab_size_spin.setValue(self.settings.tab_size)
162+
self.use_spaces_check.setChecked(self.settings.use_spaces)
163+
self.auto_indent_check.setChecked(self.settings.auto_indent)
121164

122165
def accept(self):
123166
"""Save UI values to settings and close."""
@@ -127,5 +170,8 @@ def accept(self):
127170
self.settings.build_output_bold = self.bold_check.isChecked()
128171
self.settings.incremental_builds = self.incremental_check.isChecked()
129172
self.settings.show_build_elapsed = self.elapsed_check.isChecked()
173+
self.settings.tab_size = self.tab_size_spin.value()
174+
self.settings.use_spaces = self.use_spaces_check.isChecked()
175+
self.settings.auto_indent = self.auto_indent_check.isChecked()
130176

131177
super().accept()

src/cpplab/widgets/code_editor.py

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
from PyQt6.QtWidgets import QPlainTextEdit
5-
from PyQt6.QtGui import QSyntaxHighlighter, QTextCharFormat, QColor, QFont
5+
from PyQt6.QtGui import QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QTextCursor
66
from PyQt6.QtCore import Qt, QRegularExpression, QTimer
77
from 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

Comments
 (0)