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