草庐IT

python - 如何在 QScintilla 中实现基于缩进的代码折叠?

coder 2023-05-26 原文

这里的最终目标是在 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

附言。我在互联网上发现了一段非常有趣的代码,效果很好,https://github.com/pyQode/pyqode.core/blob/master/pyqode/core/api/folding.py问题是代码打算在 QPlainTextEditQSyntaxHighlighter 上工作,所以我不太清楚如何调整它以在 QScinScintilla 中工作> 小部件

最佳答案

[删除了上一个答案,因为根据最后一个问题编辑,它可能具有的唯一值可能是历史;如果您仍然好奇,请参阅编辑历史记录]

最后是优化版——捆绑了 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_()

关于python - 如何在 QScintilla 中实现基于缩进的代码折叠?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55426342/

有关python - 如何在 QScintilla 中实现基于缩进的代码折叠?的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  3. ruby - 在 Ruby 中实现 `call_user_func_array` - 2

    我怎样才能完成http://php.net/manual/en/function.call-user-func-array.php在ruby中?所以我可以这样做:classAppdeffoo(a,b)putsa+benddefbarargs=[1,2]App.send(:foo,args)#doesn'tworkApp.send(:foo,args[0],args[1])#doeswork,butdoesnotscaleendend 最佳答案 尝试分解数组App.send(:foo,*args)

  4. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  5. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  6. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  7. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  8. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  9. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  10. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

随机推荐