Source code for eye.helpers.folding
# this project is licensed under the WTFPLv2, see COPYING.txt for details
"""Plugin for non-lexical code folding
Default lexers provide code folding, for example functions can be folded. This plugin provides folding that isn't
based on lexer tokens but on markers. For example "{{{" for starting a fold zone and "}}}" for ending a fold zone.
Simple usage:
>>> import eye.helpers.folding
>>> eye.helpers.folding.set_marker_folder.enabled = True
"""
import re
from PyQt5.Qsci import QsciScintilla
from PyQt5.QtCore import QObject, QTimer
from eye.connector import disabled, default_lexer_config, default_editor_config
from eye.qt import Slot
from eye.widgets.editor import HasWeakEditorMixin
__all__ = ('MarkerFolder', 'disable_lexer_folding', 'set_marker_folder')
[docs]
@default_lexer_config
@disabled
def disable_lexer_folding(ed, *args):
"""Disable folding based on QsciLexer for an editor widget"""
ed.set_lexer_property(b'fold', b'0')
[docs]
class MarkerFolder(QObject, HasWeakEditorMixin):
marker_start = re.compile(r'\{\{\{')
marker_end = re.compile(r'\}\}\}')
interval = 100
def __init__(self, editor=None, **kwargs):
super().__init__(**kwargs)
self.editor = editor
editor.sci_modified.connect(self.on_modification)
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.refold_queue)
self.lines_to_refold = set()
if editor:
self.refold(True)
[docs]
@Slot()
def refold(self, force=False):
self.refold_at(0, force)
[docs]
@Slot()
def refold_queue(self, force=False):
while len(self.lines_to_refold):
start = self.lines_to_refold.pop()
self.refold_at(start, force)
[docs]
def refold_at(self, start, force=False):
waitnext = True
level = self.editor.get_fold_level(start) & QsciScintilla.SC_FOLDLEVELNUMBERMASK
for i in range(start, self.editor.lines()):
self.lines_to_refold.discard(i)
flag = 0
line = self.editor.text(i)
diff = len(self.marker_start.findall(line))
if diff:
flag |= QsciScintilla.SC_FOLDLEVELHEADERFLAG
diff -= len(self.marker_end.findall(line))
new = level | flag
current = self.editor.get_fold_level(i)
if force or current != new:
self.editor.set_fold_level(i, new)
waitnext = True
else:
if not waitnext:
break
waitnext = False
level += diff
[docs]
@Slot(object)
def on_modification(self, st):
refold = None
if st.modificationType & QsciScintilla.SC_MOD_INSERTTEXT:
refold, _ = self.editor.lineIndexFromPosition(st.position)
elif st.modificationType & QsciScintilla.SC_MOD_DELETETEXT:
refold, _ = self.editor.lineIndexFromPosition(st.position)
if refold is not None:
self.lines_to_refold.add(refold)
if not self.timer.isActive():
self.timer.start(self.interval)
# TODO smarter refold: check if insert/delete contains pattern or changes folding
[docs]
@default_editor_config
@default_lexer_config
@disabled
def set_marker_folder(editor, *args):
"""Enable folding based on markers for an editor widget"""
editor.set_lexer_property(b'fold', b'0')
editor.folding = MarkerFolder(editor=editor)