Source code for simvx.core.ui.folding
"""Code folding manager for the code editor widget.
Provides indent-based fold region detection, fold/unfold operations,
and visible-line computation for folded views.
"""
from __future__ import annotations
[docs]
class FoldingMixin:
"""Mixin providing code folding for a code editor.
Expects the host class to have:
- ``_lines: list[str]``
- ``_cursor_line: int``
- ``_invalidate_cache(from_line)``
"""
def _init_folding(self):
"""Initialise folding state. Call from ``__init__``."""
self._folded_regions: dict[int, int] = {} # start_line -> end_line
self._show_fold_gutter: bool = True
# ================================================================
# Fold detection
# ================================================================
def _is_foldable(self, line: int) -> bool:
"""Check if a line can be folded (has higher-indented lines below it)."""
if line >= len(self._lines) - 1:
return False
current = self._lines[line]
if not current.strip():
return False
current_indent = len(current) - len(current.lstrip())
# Check if next non-empty line has greater indent
for i in range(line + 1, len(self._lines)):
next_line = self._lines[i]
if not next_line.strip():
continue
next_indent = len(next_line) - len(next_line.lstrip())
return next_indent > current_indent
return False
def _compute_fold_range(self, line: int) -> tuple[int, int]:
"""Compute the fold range for a line. Returns (start, end) inclusive."""
if line >= len(self._lines):
return (line, line)
current = self._lines[line]
current_indent = len(current) - len(current.lstrip())
end = line
for i in range(line + 1, len(self._lines)):
ln = self._lines[i]
if not ln.strip():
end = i
continue
indent = len(ln) - len(ln.lstrip())
if indent <= current_indent:
break
end = i
return (line, end)
# ================================================================
# Fold operations
# ================================================================
[docs]
def fold_at_line(self, line: int):
"""Fold the region starting at the given line."""
if not self._is_foldable(line) or line in self._folded_regions:
return
start, end = self._compute_fold_range(line)
if end > start:
self._folded_regions[start] = end
self._invalidate_cache()
[docs]
def unfold_at_line(self, line: int):
"""Unfold the region at the given line."""
if line in self._folded_regions:
del self._folded_regions[line]
self._invalidate_cache()
[docs]
def toggle_fold(self, line: int):
"""Toggle fold state at the given line."""
if line in self._folded_regions:
self.unfold_at_line(line)
elif self._is_foldable(line):
self.fold_at_line(line)
[docs]
def unfold_all(self):
"""Unfold all regions."""
self._folded_regions.clear()
self._invalidate_cache()
# ================================================================
# Fold queries
# ================================================================
def _is_line_folded(self, line: int) -> bool:
"""Check if a line is hidden by a fold (i.e., inside a folded region but not the start)."""
for start, end in self._folded_regions.items():
if start < line <= end:
return True
return False
def _get_visible_lines(self) -> list[int]:
"""Return list of buffer line indices that are currently visible (not folded)."""
visible = []
i = 0
while i < len(self._lines):
visible.append(i)
if i in self._folded_regions:
i = self._folded_regions[i] + 1
else:
i += 1
return visible
def _adjust_cursor_for_folds(self):
"""If cursor is on a folded (hidden) line, move to fold start."""
for start, end in self._folded_regions.items():
if start < self._cursor_line <= end:
self._cursor_line = start
return