Как реализовать свертывание кода на основе отступов в QScintilla?
Конечной целью здесь является реализация свертывания кода на основе отступов в QScintilla аналогично тому, как это делает SublimeText3.
Прежде всего, вот небольшой пример того, как вы бы вручную предоставили свертывание с помощью механизмов QScintilla:
import sys
from PyQt5.Qsci import QsciScintilla
from PyQt5.Qt import *
if __name__ == '__main__':
app = QApplication(sys.argv)
view = QsciScintilla()
# http://www.scintilla.org/ScintillaDoc.html#Folding
view.setFolding(QsciScintilla.BoxedTreeFoldStyle)
lines = [
(0, "def foo():"),
(1, " x = 10"),
(1, " y = 20"),
(1, " return x+y"),
(-1, ""),
(0, "def bar(x):"),
(1, " if x > 0:"),
(2, " print('this is')"),
(2, " print('branch1')"),
(1, " else:"),
(2, " print('and this')"),
(2, " print('is branch2')"),
(-1, ""),
(-1, ""),
(-1, ""),
(-1, "print('end')"),
]
view.setText("\n".join([b for a, b in lines]))
MASK = QsciScintilla.SC_FOLDLEVELNUMBERMASK
for i, tpl in enumerate(lines):
level, line = tpl
if level >= 0:
view.SendScintilla(view.SCI_SETFOLDLEVEL, i, level | QsciScintilla.SC_FOLDLEVELHEADERFLAG)
else:
view.SendScintilla(view.SCI_SETFOLDLEVEL, i, 0)
view.show()
app.exec_()
Чтобы узнать больше об этом, вы можете проверить официальные документы:
Ссылки на документы:
Как я уже сказал, я хотел бы реализовать свертывание кода, как это делает Sublime, поэтому я создал этот маленький mcve в качестве базового кода, чтобы поиграть:
import re
import time
from pathlib import Path
from PyQt5.Qsci import QsciLexerCustom, QsciScintilla
from PyQt5.Qt import *
def lskip_nonewlines(text, pt):
len_text = len(text)
while True:
if pt <= 0 or pt >= len_text:
break
if text[pt - 1] == "\n" or text[pt] == "\n":
break
pt -= 1
return pt
def rskip_nonewlines(text, pt):
len_text = len(text)
while True:
if pt <= 0 or pt >= len_text:
break
if text[pt] == "\n":
break
pt += 1
return pt
class Region():
__slots__ = ['a', 'b']
def __init__(self, x, b=None):
if b is None:
if isinstance(x, int):
self.a = x
self.b = x
elif isinstance(x, tuple):
self.a = x[0]
self.b = x[1]
elif isinstance(x, Region):
self.a = x.a
self.b = x.b
else:
raise TypeError(f"Can't convert {x.__class__} to Region")
else:
self.a = x
self.b = b
def __str__(self):
return "(" + str(self.a) + ", " + str(self.b) + ")"
def __repr__(self):
return "(" + str(self.a) + ", " + str(self.b) + ")"
def __len__(self):
return self.size()
def __eq__(self, rhs):
return isinstance(rhs, Region) and self.a == rhs.a and self.b == rhs.b
def __lt__(self, rhs):
lhs_begin = self.begin()
rhs_begin = rhs.begin()
if lhs_begin == rhs_begin:
return self.end() < rhs.end()
else:
return lhs_begin < rhs_begin
def __sub__(self, rhs):
if self.end() < rhs.begin():
return [self]
elif self.begin() > rhs.end():
return [self]
elif rhs.contains(self):
return []
elif self.contains(rhs):
return [Region(self.begin(), rhs.begin()), Region(rhs.end(), self.end())]
elif rhs.begin() <= self.begin():
return [Region(rhs.end(), self.end())]
elif rhs.begin() > self.begin():
return [Region(self.begin(), rhs.begin())]
else:
raise Exception("Unknown case")
def empty(self):
return self.a == self.b
def begin(self):
if self.a < self.b:
return self.a
else:
return self.b
def end(self):
if self.a < self.b:
return self.b
else:
return self.a
def size(self):
return abs(self.a - self.b)
def contains(self, x):
if isinstance(x, Region):
return self.contains(x.a) and self.contains(x.b)
else:
return x >= self.begin() and x <= self.end()
def cover(self, rhs):
a = min(self.begin(), rhs.begin())
b = max(self.end(), rhs.end())
if self.a < self.b:
return Region(a, b)
else:
return Region(b, a)
def intersection(self, rhs):
if self.end() <= rhs.begin():
return Region(0)
if self.begin() >= rhs.end():
return Region(0)
return Region(max(self.begin(), rhs.begin()), min(self.end(), rhs.end()))
def intersects(self, rhs):
lb = self.begin()
le = self.end()
rb = rhs.begin()
re = rhs.end()
return (
(lb == rb and le == re) or
(rb > lb and rb < le) or (re > lb and re < le) or
(lb > rb and lb < re) or (le > rb and le < re)
)
class View(QsciScintilla):
# -------- MAGIC FUNCTIONS --------
def __init__(self, parent=None):
super().__init__(parent)
self.tab_size = 4
# Set multiselection defaults
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
def __call__(self, prop, *args, **kwargs):
args = [v.encode("utf-8") if isinstance(v, str) else v for v in args]
kwargs = {
k: (v.encode("utf-8") if isinstance(v, str) else v)
for k, v in kwargs.items()
}
return self.SendScintilla(getattr(self, prop), *args, **kwargs)
# -------- SublimeText API --------
def size(self):
return len(self.text())
def substr(self, x):
# x = point or region
if isinstance(x, Region):
return self.text()[x.begin():x.end()]
else:
s = self.text()[x:x + 1]
if len(s) == 0:
return "\x00"
else:
return s
def line(self, x):
region = Region(x)
text = self.text()
if region.a <= region.b:
region.a = lskip_nonewlines(text, region.a)
region.b = rskip_nonewlines(text, region.b)
else:
region.a = rskip_nonewlines(text, region.a)
region.b = lskip_nonewlines(text, region.b)
return Region(region.begin(), region.end())
def full_line(self, x):
region = Region(x)
text = self.text()
if region.a <= region.b:
region.a = lskip_nonewlines(text, region.a)
region.b = rskip_nonewlines(text, region.b)
region.b = region.b + 1 if region.b < len(text) else region.b
else:
region.a = rskip_nonewlines(text, region.a)
region.b = lskip_nonewlines(text, region.b)
region.a = region.a + 1 if region.a < len(text) else region.a
return Region(region.begin(), region.end())
def indentation_level(self, pt):
view = self
r = view.full_line(pt)
line = view.substr(r)
if line == "\n":
r = view.full_line(pt - 1)
line = view.substr(r)
num_line, index = view.lineIndexFromPosition(pt)
if r.a <= 0 or r.a > view.size():
return 0
else:
i = 0
count = 0
len_line = len(line)
level = 0
while True:
if i >= len_line:
break
if line[i] == " ":
i += 1
count += 1
if count == self.tab_size:
level += 1
count = 0
elif line[i] == "\t":
level += 1
else:
break
if count != 0:
level += 1
return level
if __name__ == '__main__':
import sys
import textwrap
app = QApplication(sys.argv)
view = View()
view.setText(textwrap.dedent("""\
x - 0
x - 3
x - 3
x - 4
x - 3
x - 1
x - 2
x - 2
x - 2
x - 3
x - 3
x - 4
x - 3
x - 1
x - 4
x - 0
a
b
c
d
e
f
"""))
view.show()
app.exec_()
В приведенном выше фрагменте вы можете увидеть, что я пытался воспроизвести некоторые из функций Sublime. Если мои тесты не ошибаются, то indentation_level
должен обеспечивать тот же вывод, что и Sublime View.
ВОПРОС: Как бы вы изменили приведенный выше фрагмент, чтобы обеспечить свертывание кода на основе отступов, как в Sublime?
Здесь вы можете увидеть пример того, как работает Sublime:
И, конечно, правильный идентификатор должен также работать при использовании множественного выбора (который уже включен в вышеупомянутом mcve), пример ниже:
Вы можете видеть, как уровни сворачивания отступов обновляются идеально/эффективно при каждом изменении документа в Sublime
Настройка моей коробки:
- win7
- Python 3.6.4 (x86)
- PyQt5 == 5,12
- QScintilla == 2.10.8
Ps. Я нашел хороший интересный фрагмент кода в Интернете, который работает довольно хорошо, https://github.com/pyQode/pyqode.core/blob/master/pyqode/core/api/folding.py, проблема в том, что код предназначен для работы с QPlainTextEdit
и QSyntaxHighlighter
поэтому я не очень хорошо знаю, как настроить его для работы в виджете QScinScintilla
Ответы
Ответ 1
[стер предыдущий ответ, так как в свете последнего вопроса редактирования единственное значение, которое он, вероятно, может иметь, является историческим; обратитесь к истории редактирования, если вам все еще интересно]
Наконец, оптимизированная версия - в комплекте с 80 килограммами образца текста, чтобы показать его производительность.
from PyQt5.Qsci import QsciScintilla
from PyQt5.Qt import *
def set_fold(prev, line, fold, full):
if (prev[0] >= 0):
fmax = max(fold, prev[1])
for iter in range(prev[0], line + 1):
view.SendScintilla(view.SCI_SETFOLDLEVEL, iter,
fmax | (0, view.SC_FOLDLEVELHEADERFLAG)[iter + 1 < full])
def line_empty(line):
return view.SendScintilla(view.SCI_GETLINEENDPOSITION, line) \
<= view.SendScintilla(view.SCI_GETLINEINDENTPOSITION, line)
def modify(position, modificationType, text, length, linesAdded,
line, foldLevelNow, foldLevelPrev, token, annotationLinesAdded):
full = view.SC_MOD_INSERTTEXT | view.SC_MOD_DELETETEXT
if (~modificationType & full == full):
return
prev = [-1, 0]
full = view.SendScintilla(view.SCI_GETLINECOUNT)
lbgn = view.SendScintilla(view.SCI_LINEFROMPOSITION, position)
lend = view.SendScintilla(view.SCI_LINEFROMPOSITION, position + length)
for iter in range(max(lbgn - 1, 0), -1, -1):
if ((iter == 0) or not line_empty(iter)):
lbgn = iter
break
for iter in range(min(lend + 1, full), full + 1):
if ((iter == full) or not line_empty(iter)):
lend = min(iter + 1, full)
break
for iter in range(lbgn, lend):
if (line_empty(iter)):
if (prev[0] == -1):
prev[0] = iter
else:
fold = view.SendScintilla(view.SCI_GETLINEINDENTATION, iter)
fold //= view.SendScintilla(view.SCI_GETTABWIDTH)
set_fold(prev, iter - 1, fold, full)
set_fold([iter, fold], iter, fold, full)
prev = [-1, fold]
set_fold(prev, lend - 1, 0, full)
if __name__ == '__main__':
import sys
import textwrap
app = QApplication(sys.argv)
view = QsciScintilla()
view.SendScintilla(view.SCI_SETMULTIPLESELECTION, True)
view.SendScintilla(view.SCI_SETMULTIPASTE, 1)
view.SendScintilla(view.SCI_SETADDITIONALSELECTIONTYPING, True)
view.SendScintilla(view.SCI_SETINDENTATIONGUIDES, view.SC_IV_REAL);
view.SendScintilla(view.SCI_SETTABWIDTH, 4)
view.setFolding(view.BoxedFoldStyle)
view.SCN_MODIFIED.connect(modify)
NUM_CHUNKS = 20000
chunk = textwrap.dedent("""\
x = 1
x = 2
x = 3
""")
view.setText("\n".join([chunk for i in range(NUM_CHUNKS)]))
view.show()
app.exec_()