Как реализовать функцию комментариев, которая работает с множественным выбором в 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 находятся здесь:

Прямо сейчас эта функция поддерживает только один выбор/курсор, и способ комментирования действительно уродлив. Как вы можете видеть в коде, если вы нажмете ctrl при нажатии мыши, вы уже сможете создать несколько курсоров/выделений.

Есть несколько вещей, которых я не знаю, как достичь прямо сейчас:

1) Я бы хотел, чтобы комментарии стали хорошо выровненными, то есть они должны начинаться с одного уровня отступа. Существующая функция прямо сейчас производит уродливые невыровненные комментарии, пример того, что я называю «хорошо выровненными» комментариями:

введите описание изображения здесь

2) Сейчас рассматривается только один курсор/выделение. Как мне перебрать курсоры/выделения, чтобы применить функцию toggle_selection?

введите описание изображения здесь

введите описание изображения здесь

3) Я предполагаю, что если вы перебираете выборки, результатом будет то, что четное количество курсоров в определенной строке не будет комментировать строку (комментировать, раскомментировать), например, что-то вроде этого:

введите описание изображения здесь

4) Нечетное количество курсоров в определенной строке повлияет на строку, потому что (comment, uncomment, comment), например, что-то вроде этого:

введите описание изображения здесь

5) Если вы перебираете курсоры/выборы, вы получите вывод, как показано ниже.

введите описание изображения здесь

РЕДАКТИРОВАТЬ: 1-й проект

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 кажется довольно сложным :/


person BPL    schedule 15.05.2018    source источник


Ответы (3)


Вот простой пример подкласса редактора QsciScintilla с добавлением комментариев, подобных SublimeText, путем установки нескольких вариантов выбора с помощью Ctrl+Mouse, а затем нажатия Ctrl+K.

ОБНОВЛЕНИЕ: Обновлено комментирование, чтобы комментировать/раскомментировать на минимальном уровне отступа каждого выделения и объединять соседние выделения.

# 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.

person Matic Kukovec    schedule 01.06.2018
comment
Этот ответ потребовал от Матика адских усилий, поэтому, если кому-то это нравится, пожалуйста, не забудьте вознаградить его хорошими голосами за все его усилия, заранее спасибо! - person BPL; 17.06.2018

QsciScintilla, похоже, не раскрывает всех функций Scintilla, однако через SendScintilla мы можем получить доступ к остальной части, как вы, кажется, уже обнаружили. Таким образом, класс Commenter может выглядеть примерно так (сложная часть восстанавливает выделение, когда 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()
person mnistic    schedule 20.05.2018
comment
Тывм, твой ответ очень хороший (его можно использовать как есть). Однако я заметил несколько отличий при сравнении вашей функции с SublimeText3. Разница 1, Difference2, а также посмотрите, как в sublimetext3 есть пробел после символа комментария // что упрощает регулярное выражение/парсинг. Я буду принимать и давать награды за окончательный ответ, который больше похож на ответ Sublime. Ваш серьезный кандидат на финальный приз, +1 - person BPL; 20.05.2018
comment
Кроме того, после дальнейшего тестирования я заметил, что ваша функция не будет правильно обрабатывать свернутые области, например: она работает в этом простом case правильно, но в других случаях код содержит ошибки, Разница3 - person BPL; 20.05.2018
comment
@BPL, Difference3 может быть потому, что в QScintilla вам нужно выбрать до следующей строки, чтобы регион был выбран - person Tarun Lalwani; 21.05.2018
comment
Просто чтобы вы знали, что я решил дать полную сумму наград за этот ответ, чтобы сдержать свое обещание (я бы вознаградил самый близкий ответ, отвечающий всем требованиям), в противном случае вы получили бы только половину наград, и это было бы довольно грустно, так как вы приложили много усилий. Тем не менее, я не приму ответ как действительный из-за того, что я объяснил в своих предыдущих комментариях. - person BPL; 25.05.2018

Следующее должно продемонстрировать, где вы ошиблись с циклом. Я оставлю вам реализовать функцию переключения.

ОБНОВЛЕНО, чтобы включить полный исходный код из исходного сообщения и переместить комментарии в класс комментаторов.

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()
person shao.lo    schedule 19.05.2018
comment
Прежде всего, спасибо за усилия, но я не понимаю, как я мог бы вообще использовать ваши опубликованные функции для реализации всей функции. Давайте на мгновение забудем тот факт, что ваш ответ не охватывает ни одного из опубликованных требований в вопросе... он не охватывает даже один из самых простых случаев. Пожалуйста, посмотрите здесь -› до и после. После нескольких попыток мне кажется, что это просто глючный код. Спасибо, в любом случае! - person BPL; 20.05.2018
comment
Вам вообще не нужно публиковать мой пример кода, при тестировании вашего кода я использовал ваши функции в QScintilla, например это. Теперь сравните правильное поведение того, чего я пытаюсь добиться здесь с тем, как ведет себя ваш код , надеюсь, это немного проясняет, что я имел в виду под ошибочным кодом :). Каким образом ваш код поможет мне добиться ожидаемого поведения? - person BPL; 20.05.2018
comment
Я обновил, чтобы заменить комментарии, сделанные с помощью Ctrl + 7, версией, которую я опубликовал. - person shao.lo; 20.05.2018
comment
Множественный выбор целых строк должен работать. Я не рассматривал несколько вариантов выбора в одной строке или восстановление выбора после комментирования. - person shao.lo; 20.05.2018
comment
Проверьте еще раз мой предыдущий комментарий, чтобы понять, почему я не приму текущий ответ... Как объяснено, я не вижу, как ваш код (в текущей форме) поможет достичь окончательного цель. Кроме того, посмотрите в видео, как хорошо обновляются/сохраняются выборки при переключении. - person BPL; 20.05.2018