最终目的是与SublimeText3相似,在QScintilla中实现基于缩进的代码折叠。
首先,这是一个使用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中每个文档的更改如何完美/高效地更新缩进折叠级别
我的盒子的设置:
Ps。我在互联网上发现了一个很好的有趣代码,可以很好地工作,https://github.com/pyQode/pyqode.core/blob/master/pyqode/core/api/folding.py的问题是该代码旨在在QPlainTextEdit
和QSyntaxHighlighter
上工作,所以我不知道如何调整它使其在QScinScintilla
小部件中工作
答案 0 :(得分:10)
[擦除了先前的答案,因为鉴于最后一个问题,编辑它唯一可能具有的历史价值;如果仍然感到好奇,请参阅编辑历史记录。
最后,优化版本-捆绑了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_()