如何在 QScintilla 中实现适用于多选的评论功能?
How to implement a comment feature that works with multiple selections in QScintilla?
我正在尝试在 QScintilla 中实现一个适用于多选的切换评论功能。不幸的是我不太清楚该怎么做,到目前为止我已经想出了这个代码:
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def is_commented_line(self, line):
return line.strip().startswith(self.comment_str)
def toggle_comment_block(self):
sci = self.sci
line, index = sci.getCursorPosition()
if sci.hasSelectedText() and self.is_commented_line(sci.text(sci.getSelection()[0])):
self.uncomment_line_or_selection()
elif not self.is_commented_line(sci.text(line)):
self.comment_line_or_selection()
else:
start_line = line
while start_line > 0 and self.is_commented_line(sci.text(start_line - 1)):
start_line -= 1
end_line = line
lines = sci.lines()
while end_line < lines and self.is_commented_line(sci.text(end_line + 1)):
end_line += 1
sci.setSelection(start_line, 0, end_line, sci.lineLength(end_line))
self.uncomment_line_or_selection()
sci.setCursorPosition(line, index - len(self.comment_str))
def comment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.comment_selection()
else:
self.comment_line()
def uncomment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.uncomment_selection()
else:
self.uncomment_line()
def comment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
sci.beginUndoAction()
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.endUndoAction()
def uncomment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
if not self.is_commented_line(sci.text(line)):
return
sci.beginUndoAction()
sci.setSelection(
line, sci.indentation(line),
line, sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
sci.endUndoAction()
def comment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.setSelection(line_from, 0, end_line + 1, 0)
sci.endUndoAction()
def uncomment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
if not self.is_commented_line(sci.text(line)):
continue
sci.setSelection(
line, sci.indentation(line),
line,
sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
if line == line_from:
index_from -= len(self.comment_str)
if index_from < 0:
index_from = 0
if line == line_to:
index_to -= len(self.comment_str)
if index_to < 0:
index_to = 0
sci.setSelection(line_from, index_from, line_to, index_to)
sci.endUndoAction()
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
QShortcut(QKeySequence("Ctrl+7"), self,
self.commenter.toggle_comment_block)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
相关 Qscintilla 文档位于此处:
目前功能只支持一个selection/cursor而且评论方式真的很丑。正如您在代码中看到的,如果您在按住鼠标的同时按下 ctrl,您将能够创建多个 cursors/selections。
虽然现在有几件事我不知道如何实现:
1) 我希望注释对齐良好,也就是说,它们应该以相同的缩进级别开始。现有功能现在会产生丑陋的未对齐评论,我称之为 "well-aligned" 评论的示例:
2) 目前只考虑一个 cursor/selection。如何遍历 cursors/selections 以应用 toggle_selection 函数?
3) 我想如果你循环选择,结果将是在特定行中有偶数个光标不会注释该行(注释,取消注释),例如,像这样的东西:
4) 特定行中的奇数个光标会影响该行,因为(注释、取消注释、注释),例如,如下所示:
5) 如果你遍历 cursors/selections 你最终会产生如下所示的输出。
编辑:第一稿
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def selections(self):
regions = []
for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS)):
regions.append({
'begin': self.selection_start(i),
'end': self.selection_end(i)
})
return regions
def selection_start(self, selection):
return self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, selection)
def selection_end(self, selection):
return self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, selection)
def text(self, *args):
return self.sci.text(*args)
def run(self):
send_scintilla = self.sci.SendScintilla
for region in self.selections():
print(region)
print(repr(self.text(region['begin'],region['end'])))
EDIT2:我发现我尝试实现的这个功能的源代码在 SublimeText Default.sublime-package(zip 文件),comments.py 上可用。该代码不仅支持普通注释 //
,还支持块注释 /* ... */
。主要问题是将该代码移植到 QScintilla 似乎非常棘手:/
下面应该说明循环出错的地方。我将留给您实现切换功能。
已更新以包含原始 post 的完整源代码并将评论移至评论者 class。
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def is_commented_line(self, line):
return line.strip().startswith(self.comment_str)
def toggle_comment_block(self):
sci = self.sci
line, index = sci.getCursorPosition()
if sci.hasSelectedText() and self.is_commented_line(sci.text(sci.getSelection()[0])):
self.uncomment_line_or_selection()
elif not self.is_commented_line(sci.text(line)):
self.comment_line_or_selection()
else:
start_line = line
while start_line > 0 and self.is_commented_line(sci.text(start_line - 1)):
start_line -= 1
end_line = line
lines = sci.lines()
while end_line < lines and self.is_commented_line(sci.text(end_line + 1)):
end_line += 1
sci.setSelection(start_line, 0, end_line, sci.lineLength(end_line))
self.uncomment_line_or_selection()
sci.setCursorPosition(line, index - len(self.comment_str))
def comment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.comment_selection()
else:
self.comment_line()
def uncomment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.uncomment_selection()
else:
self.uncomment_line()
def comment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
sci.beginUndoAction()
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.endUndoAction()
def uncomment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
if not self.is_commented_line(sci.text(line)):
return
sci.beginUndoAction()
sci.setSelection(
line, sci.indentation(line),
line, sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
sci.endUndoAction()
def comment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.setSelection(line_from, 0, end_line + 1, 0)
sci.endUndoAction()
def uncomment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
if not self.is_commented_line(sci.text(line)):
continue
sci.setSelection(
line, sci.indentation(line),
line,
sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
if line == line_from:
index_from -= len(self.comment_str)
if index_from < 0:
index_from = 0
if line == line_to:
index_to -= len(self.comment_str)
if index_to < 0:
index_to = 0
sci.setSelection(line_from, index_from, line_to, index_to)
sci.endUndoAction()
def comment_blocks(self):
sci = self.sci
comment_chars = self.comment_str
selections = [(sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, i), sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, i)) for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS))]
def block_indentation(lineFrom, lineTo):
"""Get the minimum indentation for the line range"""
indent = min(sci.indentation(line) for line in range(lineFrom, lineTo))
return indent
def comment(selFrom, selTo):
lineFrom = selFrom[0]
lineTo = selTo[0] + 1
indent = block_indentation(lineFrom, lineTo)
for line in range(lineFrom, lineTo):
text = sci.text(line).lstrip()
if not text:
sci.insertAt(' ' * indent + comment_chars, line, 0) # Make sure blank lines are preserved
else:
sci.insertAt(comment_chars, line, indent)
# sci.setSelection(lineFrom, selFrom[1], lineTo, selTo[1]) # Restore selection TODO: for muliple selections see init_test_selections()
sci.beginUndoAction()
for sel in reversed(selections): # Positions will change due to inserted comment chars..so run loop in reverse
sel_from = sci.lineIndexFromPosition(sel[0])
sel_to = sci.lineIndexFromPosition(sel[1])
comment(sel_from, sel_to)
sci.endUndoAction()
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
# QShortcut(QKeySequence("Ctrl+7"), self, self.commenter.toggle_comment_block)
QShortcut(QKeySequence("Ctrl+7"), self, self.commenter.comment_blocks)
def init_test_selections(self):
# initialize multiple selections
offset1 = self.positionFromLineIndex(21, 0)
offset2 = self.positionFromLineIndex(29, 5)
self.SendScintilla(self.SCI_SETSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(31, 0)
offset2 = self.positionFromLineIndex(33, 20)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(37, 0)
offset2 = self.positionFromLineIndex(39, 5)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
def init_test_selections2(self):
# initialize multiple selections
offset1 = self.positionFromLineIndex(11, 0)
offset2 = self.positionFromLineIndex(13, 1)
self.SendScintilla(self.SCI_SETSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(15, 0)
offset2 = self.positionFromLineIndex(17, 1)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.init_test_selections()
# ex.init_test_selections2()
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
QsciScintilla
似乎没有公开 Scintilla 的所有功能,但是通过 SendScintilla
我们可以访问它的其余部分,正如您似乎已经发现的那样。所以 Commenter
class 可能看起来像这样(棘手的部分是恢复选择,因为 Scintilla 在插入时取消选择):
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
self.sel_regions = []
def toggle_comments(self):
lines = self.selected_lines()
if len(lines) <= 0:
return
all_commented = True
for line in lines:
if not self.sci.text(line).strip().startswith(self.comment_str):
all_commented = False
if not all_commented:
self.comment_lines(lines)
else:
self.uncomment_lines(lines)
def selections(self):
regions = []
for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS)):
regions.append({
'begin': self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, i),
'end': self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, i)
})
return regions
def selected_lines(self):
self.sel_regions = []
all_lines = []
regions = self.selections()
for r in regions:
start_line = self.sci.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, r['begin'])
end_line = self.sci.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, r['end'])
for cur_line in range(start_line, end_line + 1):
if not cur_line in all_lines:
all_lines.append(cur_line)
if r['begin'] <= r['end']:
self.sel_regions.append(r)
return all_lines
def comment_lines(self, lines):
indent = self.sci.indentation(lines[0])
for line in lines:
indent = min(indent, self.sci.indentation(line))
self.sci.beginUndoAction()
for line in lines:
self.adjust_selections(line, indent)
self.sci.insertAt(self.comment_str, line, indent)
self.sci.endUndoAction()
self.restore_selections()
def uncomment_lines(self, lines):
self.sci.beginUndoAction()
for line in lines:
line_start = self.sci.SendScintilla(QsciScintilla.SCI_POSITIONFROMLINE, line)
line_end = self.sci.SendScintilla(QsciScintilla.SCI_GETLINEENDPOSITION, line)
if line_start == line_end:
continue
if line_end - line_start < len(self.comment_str):
continue
done = False
for c in range(line_start, line_end - len(self.comment_str) + 1):
source_str = self.sci.text(c, c + len(self.comment_str))
if(source_str == self.comment_str):
self.sci.SendScintilla(QsciScintilla.SCI_DELETERANGE, c, len(self.comment_str))
break
self.sci.endUndoAction()
def restore_selections(self):
if(len(self.sel_regions) > 0):
first = True
for r in self.sel_regions:
if first:
self.sci.SendScintilla(QsciScintilla.SCI_SETSELECTION, r['begin'], r['end'])
first = False
else:
self.sci.SendScintilla(QsciScintilla.SCI_ADDSELECTION, r['begin'], r['end'])
def adjust_selections(self, line, indent):
for r in self.sel_regions:
if self.sci.positionFromLineIndex(line, indent) <= r['begin']:
r['begin'] += len(self.comment_str)
r['end'] += len(self.comment_str)
elif self.sci.positionFromLineIndex(line, indent) < r['end']:
r['end'] += len(self.comment_str)
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
QShortcut(QKeySequence("Ctrl+7"), self,
self.commenter.toggle_comments)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
这是一个子类 QsciScintilla 编辑器的简单示例,它添加了类似 SublimeText 的注释,方法是使用 Ctrl+Mouse
设置多个选择,然后按 Ctrl+K
。
更新:
将每个选择的最小缩进级别的注释更新为 comment/uncomment 并合并相邻的选择。
# Import the PyQt5 module with some of the GUI widgets
import PyQt5.QtWidgets
import PyQt5.QtGui
import PyQt5.QtCore
# Import the QScintilla module
import PyQt5.Qsci
# Import Python's sys module needed to get the application arguments
import sys
"""
Custom editor with a simple commenting feature
similar to what SublimeText does
"""
class MyCommentingEditor(PyQt5.Qsci.QsciScintilla):
comment_string = "// "
line_ending = "\n"
def keyPressEvent(self, event):
# Execute the superclasses event
super().keyPressEvent(event)
# Check pressed key information
key = event.key()
key_modifiers = PyQt5.QtWidgets.QApplication.keyboardModifiers()
if (key == PyQt5.QtCore.Qt.Key_K and
key_modifiers == PyQt5.QtCore.Qt.ControlModifier):
self.toggle_commenting()
def toggle_commenting(self):
# Check if the selections are valid
selections = self.get_selections()
if selections == None:
return
# Merge overlapping selections
while self.merge_test(selections) == True:
selections = self.merge_selections(selections)
# Start the undo action that can undo all commenting at once
self.beginUndoAction()
# Loop over selections and comment them
for i, sel in enumerate(selections):
if self.text(sel[0]).lstrip().startswith(self.comment_string):
self.set_commenting(sel[0], sel[1], self._uncomment)
else:
self.set_commenting(sel[0], sel[1], self._comment)
# Select back the previously selected regions
self.SendScintilla(self.SCI_CLEARSELECTIONS)
for i, sel in enumerate(selections):
start_index = self.positionFromLineIndex(sel[0], 0)
# Check if ending line is the last line in the editor
last_line = sel[1]
if last_line == self.lines() - 1:
end_index = self.positionFromLineIndex(sel[1], len(self.text(last_line)))
else:
end_index = self.positionFromLineIndex(sel[1], len(self.text(last_line))-1)
if i == 0:
self.SendScintilla(self.SCI_SETSELECTION, start_index, end_index)
else:
self.SendScintilla(self.SCI_ADDSELECTION, start_index, end_index)
# Set the end of the undo action
self.endUndoAction()
def get_selections(self):
# Get the selection and store them in a list
selections = []
for i in range(self.SendScintilla(self.SCI_GETSELECTIONS)):
selection = (
self.SendScintilla(self.SCI_GETSELECTIONNSTART, i),
self.SendScintilla(self.SCI_GETSELECTIONNEND, i)
)
# Add selection to list
from_line, from_index = self.lineIndexFromPosition(selection[0])
to_line, to_index = self.lineIndexFromPosition(selection[1])
selections.append((from_line, to_line))
selections.sort()
# Return selection list
return selections
def merge_test(self, selections):
"""
Test if merging of selections is needed
"""
for i in range(1, len(selections)):
# Get the line numbers
previous_start_line = selections[i-1][0]
previous_end_line = selections[i-1][1]
current_start_line = selections[i][0]
current_end_line = selections[i][1]
if previous_end_line == current_start_line:
return True
# Merging is not needed
return False
def merge_selections(self, selections):
"""
This function merges selections with overlapping lines
"""
# Test if merging is required
if len(selections) < 2:
return selections
merged_selections = []
skip_flag = False
for i in range(1, len(selections)):
# Get the line numbers
previous_start_line = selections[i-1][0]
previous_end_line = selections[i-1][1]
current_start_line = selections[i][0]
current_end_line = selections[i][1]
# Test for merge
if previous_end_line == current_start_line and skip_flag == False:
merged_selections.append(
(previous_start_line, current_end_line)
)
skip_flag = True
else:
if skip_flag == False:
merged_selections.append(
(previous_start_line, previous_end_line)
)
skip_flag = False
# Add the last selection only if it was not merged
if i == (len(selections) - 1):
merged_selections.append(
(current_start_line, current_end_line)
)
# Return the merged selections
return merged_selections
def set_commenting(self, arg_from_line, arg_to_line, func):
# Get the cursor information
from_line = arg_from_line
to_line = arg_to_line
# Check if ending line is the last line in the editor
last_line = to_line
if last_line == self.lines() - 1:
to_index = len(self.text(to_line))
else:
to_index = len(self.text(to_line))-1
# Set the selection from the beginning of the cursor line
# to the end of the last selection line
self.setSelection(
from_line, 0, to_line, to_index
)
# Get the selected text and split it into lines
selected_text = self.selectedText()
selected_list = selected_text.split("\n")
# Find the smallest indent level
indent_levels = []
for line in selected_list:
indent_levels.append(len(line) - len(line.lstrip()))
min_indent_level = min(indent_levels)
# Add the commenting character to every line
for i, line in enumerate(selected_list):
selected_list[i] = func(line, min_indent_level)
# Replace the whole selected text with the merged lines
# containing the commenting characters
replace_text = self.line_ending.join(selected_list)
self.replaceSelectedText(replace_text)
def _comment(self, line, indent_level):
if line.strip() != "":
return line[:indent_level] + self.comment_string + line[indent_level:]
else:
return line
def _uncomment(self, line, indent_level):
if line.strip().startswith(self.comment_string):
return line.replace(self.comment_string, "", 1)
else:
return line
有关完整示例,请参阅 https://github.com/matkuki/qscintilla_docs/blob/master/examples/commenting.py
我将 PyQt5 与 QScintilla 2.10.4 和 Python 3.6 一起使用。
我正在尝试在 QScintilla 中实现一个适用于多选的切换评论功能。不幸的是我不太清楚该怎么做,到目前为止我已经想出了这个代码:
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def is_commented_line(self, line):
return line.strip().startswith(self.comment_str)
def toggle_comment_block(self):
sci = self.sci
line, index = sci.getCursorPosition()
if sci.hasSelectedText() and self.is_commented_line(sci.text(sci.getSelection()[0])):
self.uncomment_line_or_selection()
elif not self.is_commented_line(sci.text(line)):
self.comment_line_or_selection()
else:
start_line = line
while start_line > 0 and self.is_commented_line(sci.text(start_line - 1)):
start_line -= 1
end_line = line
lines = sci.lines()
while end_line < lines and self.is_commented_line(sci.text(end_line + 1)):
end_line += 1
sci.setSelection(start_line, 0, end_line, sci.lineLength(end_line))
self.uncomment_line_or_selection()
sci.setCursorPosition(line, index - len(self.comment_str))
def comment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.comment_selection()
else:
self.comment_line()
def uncomment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.uncomment_selection()
else:
self.uncomment_line()
def comment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
sci.beginUndoAction()
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.endUndoAction()
def uncomment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
if not self.is_commented_line(sci.text(line)):
return
sci.beginUndoAction()
sci.setSelection(
line, sci.indentation(line),
line, sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
sci.endUndoAction()
def comment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.setSelection(line_from, 0, end_line + 1, 0)
sci.endUndoAction()
def uncomment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
if not self.is_commented_line(sci.text(line)):
continue
sci.setSelection(
line, sci.indentation(line),
line,
sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
if line == line_from:
index_from -= len(self.comment_str)
if index_from < 0:
index_from = 0
if line == line_to:
index_to -= len(self.comment_str)
if index_to < 0:
index_to = 0
sci.setSelection(line_from, index_from, line_to, index_to)
sci.endUndoAction()
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
QShortcut(QKeySequence("Ctrl+7"), self,
self.commenter.toggle_comment_block)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
相关 Qscintilla 文档位于此处:
目前功能只支持一个selection/cursor而且评论方式真的很丑。正如您在代码中看到的,如果您在按住鼠标的同时按下 ctrl,您将能够创建多个 cursors/selections。
虽然现在有几件事我不知道如何实现:
1) 我希望注释对齐良好,也就是说,它们应该以相同的缩进级别开始。现有功能现在会产生丑陋的未对齐评论,我称之为 "well-aligned" 评论的示例:
2) 目前只考虑一个 cursor/selection。如何遍历 cursors/selections 以应用 toggle_selection 函数?
3) 我想如果你循环选择,结果将是在特定行中有偶数个光标不会注释该行(注释,取消注释),例如,像这样的东西:
4) 特定行中的奇数个光标会影响该行,因为(注释、取消注释、注释),例如,如下所示:
5) 如果你遍历 cursors/selections 你最终会产生如下所示的输出。
编辑:第一稿
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def selections(self):
regions = []
for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS)):
regions.append({
'begin': self.selection_start(i),
'end': self.selection_end(i)
})
return regions
def selection_start(self, selection):
return self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, selection)
def selection_end(self, selection):
return self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, selection)
def text(self, *args):
return self.sci.text(*args)
def run(self):
send_scintilla = self.sci.SendScintilla
for region in self.selections():
print(region)
print(repr(self.text(region['begin'],region['end'])))
EDIT2:我发现我尝试实现的这个功能的源代码在 SublimeText Default.sublime-package(zip 文件),comments.py 上可用。该代码不仅支持普通注释 //
,还支持块注释 /* ... */
。主要问题是将该代码移植到 QScintilla 似乎非常棘手:/
下面应该说明循环出错的地方。我将留给您实现切换功能。
已更新以包含原始 post 的完整源代码并将评论移至评论者 class。
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
def is_commented_line(self, line):
return line.strip().startswith(self.comment_str)
def toggle_comment_block(self):
sci = self.sci
line, index = sci.getCursorPosition()
if sci.hasSelectedText() and self.is_commented_line(sci.text(sci.getSelection()[0])):
self.uncomment_line_or_selection()
elif not self.is_commented_line(sci.text(line)):
self.comment_line_or_selection()
else:
start_line = line
while start_line > 0 and self.is_commented_line(sci.text(start_line - 1)):
start_line -= 1
end_line = line
lines = sci.lines()
while end_line < lines and self.is_commented_line(sci.text(end_line + 1)):
end_line += 1
sci.setSelection(start_line, 0, end_line, sci.lineLength(end_line))
self.uncomment_line_or_selection()
sci.setCursorPosition(line, index - len(self.comment_str))
def comment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.comment_selection()
else:
self.comment_line()
def uncomment_line_or_selection(self):
sci = self.sci
if sci.hasSelectedText():
self.uncomment_selection()
else:
self.uncomment_line()
def comment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
sci.beginUndoAction()
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.endUndoAction()
def uncomment_line(self):
sci = self.sci
line, index = sci.getCursorPosition()
if not self.is_commented_line(sci.text(line)):
return
sci.beginUndoAction()
sci.setSelection(
line, sci.indentation(line),
line, sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
sci.endUndoAction()
def comment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
sci.insertAt(self.comment_str, line, sci.indentation(line))
sci.setSelection(line_from, 0, end_line + 1, 0)
sci.endUndoAction()
def uncomment_selection(self):
sci = self.sci
if not sci.hasSelectedText():
return
line_from, index_from, line_to, index_to = sci.getSelection()
if index_to == 0:
end_line = line_to - 1
else:
end_line = line_to
sci.beginUndoAction()
for line in range(line_from, end_line + 1):
if not self.is_commented_line(sci.text(line)):
continue
sci.setSelection(
line, sci.indentation(line),
line,
sci.indentation(line) + len(self.comment_str)
)
sci.removeSelectedText()
if line == line_from:
index_from -= len(self.comment_str)
if index_from < 0:
index_from = 0
if line == line_to:
index_to -= len(self.comment_str)
if index_to < 0:
index_to = 0
sci.setSelection(line_from, index_from, line_to, index_to)
sci.endUndoAction()
def comment_blocks(self):
sci = self.sci
comment_chars = self.comment_str
selections = [(sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, i), sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, i)) for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS))]
def block_indentation(lineFrom, lineTo):
"""Get the minimum indentation for the line range"""
indent = min(sci.indentation(line) for line in range(lineFrom, lineTo))
return indent
def comment(selFrom, selTo):
lineFrom = selFrom[0]
lineTo = selTo[0] + 1
indent = block_indentation(lineFrom, lineTo)
for line in range(lineFrom, lineTo):
text = sci.text(line).lstrip()
if not text:
sci.insertAt(' ' * indent + comment_chars, line, 0) # Make sure blank lines are preserved
else:
sci.insertAt(comment_chars, line, indent)
# sci.setSelection(lineFrom, selFrom[1], lineTo, selTo[1]) # Restore selection TODO: for muliple selections see init_test_selections()
sci.beginUndoAction()
for sel in reversed(selections): # Positions will change due to inserted comment chars..so run loop in reverse
sel_from = sci.lineIndexFromPosition(sel[0])
sel_to = sci.lineIndexFromPosition(sel[1])
comment(sel_from, sel_to)
sci.endUndoAction()
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
# QShortcut(QKeySequence("Ctrl+7"), self, self.commenter.toggle_comment_block)
QShortcut(QKeySequence("Ctrl+7"), self, self.commenter.comment_blocks)
def init_test_selections(self):
# initialize multiple selections
offset1 = self.positionFromLineIndex(21, 0)
offset2 = self.positionFromLineIndex(29, 5)
self.SendScintilla(self.SCI_SETSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(31, 0)
offset2 = self.positionFromLineIndex(33, 20)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(37, 0)
offset2 = self.positionFromLineIndex(39, 5)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
def init_test_selections2(self):
# initialize multiple selections
offset1 = self.positionFromLineIndex(11, 0)
offset2 = self.positionFromLineIndex(13, 1)
self.SendScintilla(self.SCI_SETSELECTION, offset1, offset2)
offset1 = self.positionFromLineIndex(15, 0)
offset2 = self.positionFromLineIndex(17, 1)
self.SendScintilla(self.SCI_ADDSELECTION, offset1, offset2)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.init_test_selections()
# ex.init_test_selections2()
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
QsciScintilla
似乎没有公开 Scintilla 的所有功能,但是通过 SendScintilla
我们可以访问它的其余部分,正如您似乎已经发现的那样。所以 Commenter
class 可能看起来像这样(棘手的部分是恢复选择,因为 Scintilla 在插入时取消选择):
import sys
import re
import math
from PyQt5.Qt import * # noqa
from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP
class Commenter():
def __init__(self, sci, comment_str):
self.sci = sci
self.comment_str = comment_str
self.sel_regions = []
def toggle_comments(self):
lines = self.selected_lines()
if len(lines) <= 0:
return
all_commented = True
for line in lines:
if not self.sci.text(line).strip().startswith(self.comment_str):
all_commented = False
if not all_commented:
self.comment_lines(lines)
else:
self.uncomment_lines(lines)
def selections(self):
regions = []
for i in range(self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONS)):
regions.append({
'begin': self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNSTART, i),
'end': self.sci.SendScintilla(QsciScintilla.SCI_GETSELECTIONNEND, i)
})
return regions
def selected_lines(self):
self.sel_regions = []
all_lines = []
regions = self.selections()
for r in regions:
start_line = self.sci.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, r['begin'])
end_line = self.sci.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, r['end'])
for cur_line in range(start_line, end_line + 1):
if not cur_line in all_lines:
all_lines.append(cur_line)
if r['begin'] <= r['end']:
self.sel_regions.append(r)
return all_lines
def comment_lines(self, lines):
indent = self.sci.indentation(lines[0])
for line in lines:
indent = min(indent, self.sci.indentation(line))
self.sci.beginUndoAction()
for line in lines:
self.adjust_selections(line, indent)
self.sci.insertAt(self.comment_str, line, indent)
self.sci.endUndoAction()
self.restore_selections()
def uncomment_lines(self, lines):
self.sci.beginUndoAction()
for line in lines:
line_start = self.sci.SendScintilla(QsciScintilla.SCI_POSITIONFROMLINE, line)
line_end = self.sci.SendScintilla(QsciScintilla.SCI_GETLINEENDPOSITION, line)
if line_start == line_end:
continue
if line_end - line_start < len(self.comment_str):
continue
done = False
for c in range(line_start, line_end - len(self.comment_str) + 1):
source_str = self.sci.text(c, c + len(self.comment_str))
if(source_str == self.comment_str):
self.sci.SendScintilla(QsciScintilla.SCI_DELETERANGE, c, len(self.comment_str))
break
self.sci.endUndoAction()
def restore_selections(self):
if(len(self.sel_regions) > 0):
first = True
for r in self.sel_regions:
if first:
self.sci.SendScintilla(QsciScintilla.SCI_SETSELECTION, r['begin'], r['end'])
first = False
else:
self.sci.SendScintilla(QsciScintilla.SCI_ADDSELECTION, r['begin'], r['end'])
def adjust_selections(self, line, indent):
for r in self.sel_regions:
if self.sci.positionFromLineIndex(line, indent) <= r['begin']:
r['begin'] += len(self.comment_str)
r['end'] += len(self.comment_str)
elif self.sci.positionFromLineIndex(line, indent) < r['end']:
r['end'] += len(self.comment_str)
class Foo(QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
# http://www.scintilla.org/ScintillaDoc.html#Folding
self.setFolding(QsciScintilla.BoxedTreeFoldStyle)
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
self.setIndentationGuides(True)
# Set the default font
self.font = QFont()
self.font.setFamily('Consolas')
self.font.setFixedPitch(True)
self.font.setPointSize(10)
self.setFont(self.font)
self.setMarginsFont(self.font)
# Margin 0 is used for line numbers
fontmetrics = QFontMetrics(self.font)
self.setMarginsFont(self.font)
self.setMarginWidth(0, fontmetrics.width("000") + 6)
self.setMarginLineNumbers(0, True)
self.setMarginsBackgroundColor(QColor("#cccccc"))
# Indentation
self.setIndentationsUseTabs(False)
self.setIndentationWidth(4)
self.setBackspaceUnindents(True)
lexer = QsciLexerCPP()
lexer.setFoldAtElse(True)
lexer.setFoldComments(True)
lexer.setFoldCompact(False)
lexer.setFoldPreprocessor(True)
self.setLexer(lexer)
# Use raw messages to Scintilla here
# (all messages are documented here: http://www.scintilla.org/ScintillaDoc.html)
# Ensure the width of the currently visible lines can be scrolled
self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)
# Multiple cursor support
self.SendScintilla(QsciScintilla.SCI_SETMULTIPLESELECTION, True)
self.SendScintilla(QsciScintilla.SCI_SETMULTIPASTE, 1)
self.SendScintilla(
QsciScintilla.SCI_SETADDITIONALSELECTIONTYPING, True)
# Comment feature goes here
self.commenter = Commenter(self, "//")
QShortcut(QKeySequence("Ctrl+7"), self,
self.commenter.toggle_comments)
def main():
app = QApplication(sys.argv)
ex = Foo()
ex.setText("""\
#include <iostream>
using namespace std;
void Function0() {
cout << "Function0";
}
void Function1() {
cout << "Function1";
}
void Function2() {
cout << "Function2";
}
void Function3() {
cout << "Function3";
}
int main(void) {
if (1) {
if (1) {
if (1) {
if (1) {
int yay;
}
}
}
}
if (1) {
if (1) {
if (1) {
if (1) {
int yay2;
}
}
}
}
return 0;
}\
""")
ex.resize(800, 600)
ex.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
这是一个子类 QsciScintilla 编辑器的简单示例,它添加了类似 SublimeText 的注释,方法是使用 Ctrl+Mouse
设置多个选择,然后按 Ctrl+K
。
更新: 将每个选择的最小缩进级别的注释更新为 comment/uncomment 并合并相邻的选择。
# Import the PyQt5 module with some of the GUI widgets
import PyQt5.QtWidgets
import PyQt5.QtGui
import PyQt5.QtCore
# Import the QScintilla module
import PyQt5.Qsci
# Import Python's sys module needed to get the application arguments
import sys
"""
Custom editor with a simple commenting feature
similar to what SublimeText does
"""
class MyCommentingEditor(PyQt5.Qsci.QsciScintilla):
comment_string = "// "
line_ending = "\n"
def keyPressEvent(self, event):
# Execute the superclasses event
super().keyPressEvent(event)
# Check pressed key information
key = event.key()
key_modifiers = PyQt5.QtWidgets.QApplication.keyboardModifiers()
if (key == PyQt5.QtCore.Qt.Key_K and
key_modifiers == PyQt5.QtCore.Qt.ControlModifier):
self.toggle_commenting()
def toggle_commenting(self):
# Check if the selections are valid
selections = self.get_selections()
if selections == None:
return
# Merge overlapping selections
while self.merge_test(selections) == True:
selections = self.merge_selections(selections)
# Start the undo action that can undo all commenting at once
self.beginUndoAction()
# Loop over selections and comment them
for i, sel in enumerate(selections):
if self.text(sel[0]).lstrip().startswith(self.comment_string):
self.set_commenting(sel[0], sel[1], self._uncomment)
else:
self.set_commenting(sel[0], sel[1], self._comment)
# Select back the previously selected regions
self.SendScintilla(self.SCI_CLEARSELECTIONS)
for i, sel in enumerate(selections):
start_index = self.positionFromLineIndex(sel[0], 0)
# Check if ending line is the last line in the editor
last_line = sel[1]
if last_line == self.lines() - 1:
end_index = self.positionFromLineIndex(sel[1], len(self.text(last_line)))
else:
end_index = self.positionFromLineIndex(sel[1], len(self.text(last_line))-1)
if i == 0:
self.SendScintilla(self.SCI_SETSELECTION, start_index, end_index)
else:
self.SendScintilla(self.SCI_ADDSELECTION, start_index, end_index)
# Set the end of the undo action
self.endUndoAction()
def get_selections(self):
# Get the selection and store them in a list
selections = []
for i in range(self.SendScintilla(self.SCI_GETSELECTIONS)):
selection = (
self.SendScintilla(self.SCI_GETSELECTIONNSTART, i),
self.SendScintilla(self.SCI_GETSELECTIONNEND, i)
)
# Add selection to list
from_line, from_index = self.lineIndexFromPosition(selection[0])
to_line, to_index = self.lineIndexFromPosition(selection[1])
selections.append((from_line, to_line))
selections.sort()
# Return selection list
return selections
def merge_test(self, selections):
"""
Test if merging of selections is needed
"""
for i in range(1, len(selections)):
# Get the line numbers
previous_start_line = selections[i-1][0]
previous_end_line = selections[i-1][1]
current_start_line = selections[i][0]
current_end_line = selections[i][1]
if previous_end_line == current_start_line:
return True
# Merging is not needed
return False
def merge_selections(self, selections):
"""
This function merges selections with overlapping lines
"""
# Test if merging is required
if len(selections) < 2:
return selections
merged_selections = []
skip_flag = False
for i in range(1, len(selections)):
# Get the line numbers
previous_start_line = selections[i-1][0]
previous_end_line = selections[i-1][1]
current_start_line = selections[i][0]
current_end_line = selections[i][1]
# Test for merge
if previous_end_line == current_start_line and skip_flag == False:
merged_selections.append(
(previous_start_line, current_end_line)
)
skip_flag = True
else:
if skip_flag == False:
merged_selections.append(
(previous_start_line, previous_end_line)
)
skip_flag = False
# Add the last selection only if it was not merged
if i == (len(selections) - 1):
merged_selections.append(
(current_start_line, current_end_line)
)
# Return the merged selections
return merged_selections
def set_commenting(self, arg_from_line, arg_to_line, func):
# Get the cursor information
from_line = arg_from_line
to_line = arg_to_line
# Check if ending line is the last line in the editor
last_line = to_line
if last_line == self.lines() - 1:
to_index = len(self.text(to_line))
else:
to_index = len(self.text(to_line))-1
# Set the selection from the beginning of the cursor line
# to the end of the last selection line
self.setSelection(
from_line, 0, to_line, to_index
)
# Get the selected text and split it into lines
selected_text = self.selectedText()
selected_list = selected_text.split("\n")
# Find the smallest indent level
indent_levels = []
for line in selected_list:
indent_levels.append(len(line) - len(line.lstrip()))
min_indent_level = min(indent_levels)
# Add the commenting character to every line
for i, line in enumerate(selected_list):
selected_list[i] = func(line, min_indent_level)
# Replace the whole selected text with the merged lines
# containing the commenting characters
replace_text = self.line_ending.join(selected_list)
self.replaceSelectedText(replace_text)
def _comment(self, line, indent_level):
if line.strip() != "":
return line[:indent_level] + self.comment_string + line[indent_level:]
else:
return line
def _uncomment(self, line, indent_level):
if line.strip().startswith(self.comment_string):
return line.replace(self.comment_string, "", 1)
else:
return line
有关完整示例,请参阅 https://github.com/matkuki/qscintilla_docs/blob/master/examples/commenting.py
我将 PyQt5 与 QScintilla 2.10.4 和 Python 3.6 一起使用。