Hi guys,

In the example in the attachment (lexer_change.py) when reseting the lexer of 
the QSciScintilla editor
by pressing the button RESET LEXER on the top, the margin font gets reset to 
the default and brace matching
becomes weird (the braces move to the left when the cursor is on a brace-pair).

In the attached example this happens always, but I have code where this does 
not happen, but the code is more or less the same.
Am I missing something or is this a bug?

Specs:
Windows 10
PyQt 5.14.0 or PyQt 5.14.1
QScintilla 2.11.4

Thanks,
Matic


    
# Import the PyQt5 module with some of the GUI widgets
import PyQt5.QtWidgets
# Import the QScintilla module
import PyQt5.Qsci
# Import Python's sys module needed to get the application arguments
import sys
import re


# Custom editor
class Editor(PyQt5.Qsci.QsciScintilla):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMarginType(0, PyQt5.Qsci.QsciScintilla.NumberMargin)
        self.setMarginWidth(0, "000000")
        self.setMarginsFont(PyQt5.QtGui.QFont('Courier', 10))
        
        self.setBraceMatching(PyQt5.Qsci.QsciScintilla.SloppyBraceMatch)
        self.setMatchedBraceBackgroundColor(PyQt5.QtGui.QColor(255, 153, 0))

# Create a custom Nim lexer
class LexerNim(PyQt5.Qsci.QsciLexerCustom):
    styles = {
        "Default" : 0,
        "Keyword" : 1,
        "Unsafe" : 2,
        "MultilineComment" : 3,
    }
    keyword_list = [
        "block", "const", "export", "import", "include", "let", 
        "static", "type", "using", "var", "when", 
        "as", "atomic", "bind", "sizeof", 
        "break", "case", "continue", "converter",
        "discard", "distinct", "do", "echo", "elif", "else", "end",
        "except", "finally", "for", "from", "defined", 
        "if", "interface", "iterator", "macro", "method", "mixin", 
        "of", "out", "proc", "func", "raise", "ref", "result", 
        "return", "template", "try", "inc", "dec", "new", "quit", 
        "while", "with", "without", "yield", "true", "false", 
        "assert", "min", "max", "newseq", "len", "pred", "succ", 
        "contains", "cmp", "add", "del","deepcopy", "shallowcopy", 
        "abs", "clamp", "isnil", "open", "reopen", "close","readall", 
        "readfile", "writefile", "endoffile", "readline", "writeline",
    ]
    unsafe_keyword_list = [
        "asm", "addr", "cast", "ptr", "pointer", "alloc", "alloc0",
        "allocshared0", "dealloc", "realloc", "nil", "gc_ref", 
        "gc_unref", "copymem", "zeromem", "equalmem", "movemem", 
        "gc_disable", "gc_enable", 
    ]
    
    def __init__(self, parent=None):
        # Initialize superclass
        super().__init__(parent)
        # Set the default style values
        self.setDefaultColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x00))
        self.setDefaultPaper(PyQt5.QtGui.QColor(0xff, 0xff, 0xff))
        self.setDefaultFont(PyQt5.QtGui.QFont("Courier", 8))
        # Initialize all style colors
        self.init_colors()
        # Init the fonts
        for i in range(len(self.styles)):
            if i == self.styles["Keyword"]:
                # Make keywords bold
                self.setFont(PyQt5.QtGui.QFont("Courier", 8, 
weight=PyQt5.QtGui.QFont.Black), i)
            else:
                self.setFont(PyQt5.QtGui.QFont("Courier", 8), i)
        # Set the Keywords style to be clickable with hotspots
        # using the scintilla low level messaging system
        parent.SendScintilla(
            PyQt5.Qsci.QsciScintillaBase.SCI_STYLESETHOTSPOT, 
            self.styles["Keyword"], 
            True
        )
        parent.SendScintilla(
            PyQt5.Qsci.QsciScintillaBase.SCI_SETHOTSPOTACTIVEFORE, 
            True, 
            PyQt5.QtGui.QColor(0x00, 0x7f, 0xff)
        )
        parent.SendScintilla(
            PyQt5.Qsci.QsciScintillaBase.SCI_SETHOTSPOTACTIVEUNDERLINE, True
        )
        # Define a hotspot click function
        def hotspot_click(position, modifiers):
            """
            Simple example for getting the clicked token
            """
            text = parent.text()
            delimiters = [
                '(', ')', '[', ']', '{', '}', ' ', '.', ',', ';', '-',
                '+', '=', '/', '*', '#'
            ]
            start = 0
            end = 0
            for i in range(position+1, len(text)):
                if text[i] in delimiters:
                    end = i
                    break
            for i in range(position,-1,-1):
                if text[i] in delimiters:
                    start = i
                    break
            clicked_token = text[start:end].strip()
            # Print the token and replace it with the string "CLICK"
            print("'" + clicked_token + "'")
            parent.setSelection(0, start+1, 0, end)
            parent.replaceSelectedText("CLICK")
        # Attach the hotspot click signal to a predefined function
        parent.SCN_HOTSPOTCLICK.connect(hotspot_click)
            
    
    def init_colors(self):
        # Font color
        self.setColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x00), 
self.styles["Default"])
        self.setColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x7f), 
self.styles["Keyword"])
        self.setColor(PyQt5.QtGui.QColor(0x7f, 0x00, 0x00), 
self.styles["Unsafe"])
        self.setColor(PyQt5.QtGui.QColor(0x7f, 0x7f, 0x00), 
self.styles["MultilineComment"])
        # Paper color
        for i in range(len(self.styles)):
            self.setPaper(PyQt5.QtGui.QColor(0xff, 0xff, 0xff), i)
    
    def language(self):
        return "Nim"
    
    def description(self, style):
        if style < len(self.styles):
            description = "Custom lexer for the Nim programming languages"
        else:
            description = ""
        return description
    
    def styleText(self, start, end):
        # Initialize the styling
        self.startStyling(start)
        # Tokenize the text that needs to be styled using regular expressions.
        # To style a sequence of characters you need to know the length of the 
sequence
        # and which style you wish to apply to the sequence. It is up to the 
implementer
        # to figure out which style the sequence belongs to. 
        # THE PROCEDURE SHOWN BELOW IS JUST ONE OF MANY!
        splitter = re.compile(r"(\{\.|\.\}|\#|\'|\"\"\"|\n|\s+|\w+|\W)")
        # Scintilla works with bytes, so we have to adjust the start and end 
boundaries.
        # Like all Qt objects the lexers parent is the QScintilla editor.
        text = bytearray(self.parent().text(), 
"utf-8")[start:end].decode("utf-8")
        tokens = [
            (token, len(bytearray(token, "utf-8"))) 
                for token in splitter.findall(text)
        ]
        # Multiline styles
        multiline_comment_flag = False
        # Check previous style for a multiline style
        if start != 0:
            previous_style = editor.SendScintilla(editor.SCI_GETSTYLEAT, start 
- 1)
            if previous_style == self.styles["MultilineComment"]:
                multiline_comment_flag = True
        # Style the text in a loop
        for i, token in enumerate(tokens):
            if multiline_comment_flag == False and token[0] == "#" and 
tokens[i+1][0] == "[":
                # Start of a multiline comment
                self.setStyling(token[1], self.styles["MultilineComment"])
                # Set the multiline comment flag
                multiline_comment_flag = True
            elif multiline_comment_flag == True:
                # Multiline comment flag is set
                self.setStyling(token[1], self.styles["MultilineComment"])
                # Check if a multiline comment ends
                if token[0] == "#" and tokens[i-1][0] == "]":
                    multiline_comment_flag = False
            elif token[0] in self.keyword_list:
                # Keyword
                self.setStyling(token[1], self.styles["Keyword"])
            elif token[0] in self.unsafe_keyword_list:
                # Keyword
                self.setStyling(token[1], self.styles["Unsafe"])
            else:
                # Style with the default style
                self.setStyling(token[1], self.styles["Default"])
    

# Create the main PyQt application object
application = PyQt5.QtWidgets.QApplication(sys.argv)

# Create a QScintila editor instance
editor = Editor()
# Set the lexer to the custom Nim lexer
nim_lexer = LexerNim(editor)
editor.setLexer(nim_lexer)
# Set the initial text with some Nim code
editor.setText(
"""
proc python_style_text*(self, args: PyObjectPtr): PyObjectPtr {.exportc, 
cdecl.} =
    var
        result_object: PyObjectPtr
        value_start, value_end: cint
        lexer, editor: PyObjectPtr
        parse_result: cint
    
    parse_result = argParseTuple(
        args, 
        "iiOO", 
        addr(value_start),
        addr(value_end),
        addr(lexer),
        addr(editor),
    )
    
    #[
            if parse_result == 0:
                echo "Napaka v pretvarjanju argumentov v funkcijo!"        
                returnNone()
    ]#
    
    var
        text_method = objectGetAttr(editor, buildValue("s", cstring("text")))
        text_object = objectCallObject(text_method, tupleNew(0))
        string_object = unicodeAsEncodedString(text_object, "utf-8", nil)
        cstring_whole_text = bytesAsString(string_object)
        whole_text = $cstring_whole_text
        text = whole_text[int(value_start)..int(value_end-1)]
        text_length = len(text)
        current_token: string = ""
    # Prepare the objects that will be called as functions
    var
        start_styling_obj: PyObjectPtr
        start_args: PyObjectPtr
        set_styling_obj: PyObjectPtr
        set_args: PyObjectPtr
        send_scintilla_obj: PyObjectPtr
        send_args: PyObjectPtr
    start_styling_obj = objectGetAttr(lexer, buildValue("s", 
cstring("startStyling")))
    start_args = tupleNew(1)
    set_styling_obj = objectGetAttr(lexer, buildValue("s", 
cstring("setStyling")))
    set_args = tupleNew(2)
    send_scintilla_obj = objectGetAttr(editor, buildValue("s", 
cstring("SendScintilla")))
    send_args = tupleNew(2)
    
    # Template for final cleanup
    template clean_up() =
        xDecref(text_method)
        xDecref(text_object)
        xDecref(string_object)
        xDecref(args)
        xDecref(result_object)
        
        xDecref(start_styling_obj)
        xDecref(start_args)
        xDecref(set_styling_obj)
        xDecref(set_args)
        xDecref(send_scintilla_obj)
        xDecref(send_args)
    # Template for the lexers setStyling function
    template set_styling(length: int, style: int) =
        discard tupleSetItem(set_args, 0, buildValue("i", length))
        discard tupleSetItem(set_args, 1, buildValue("i", style))
        discard objectCallObject(set_styling_obj, set_args)
    # Procedure for getting previous style
    proc get_previous_style(): int =
        discard tupleSetItem(send_args, 0, buildValue("i", SCI_GETSTYLEAT))
        discard tupleSetItem(send_args, 1, buildValue("i", value_start - 1))
        result = longAsLong(objectCallObject(send_scintilla_obj, send_args))
        xDecref(send_args)
    # Template for starting styling
    template start_styling() =
        discard tupleSetItem(start_args, 0, buildValue("i", value_start))
        discard objectCallObject(start_styling_obj, start_args)
    # Safety
    if set_styling_obj == nil:
        raise newException(FieldError, "Lexer doesn't contain the 'setStyling' 
method!")
    elif start_styling_obj == nil:
        raise newException(FieldError, "Lexer doesn't contain the 
'startStyling' method!")
    elif send_scintilla_obj == nil:
        raise newException(FieldError, "Editor doesn't contain the 
'SendScintilla' method!")
    # Styling initialization
    start_styling()
    
#------------------------------------------------------------------------------
    var 
        actseq = SeqActive(active: false)
        token_name: string = ""
        previous_token: string = ""
        token_start: int = 0
        token_length: int = 0
    # Check previous style
    if value_start != 0:
        var previous_style = get_previous_style()
        for i in multiline_sequence_list:
            if previous_style == i.style:
                actseq.sequence = i
                actseq.active = true
                break
    # Style the tokens accordingly
    proc check_start_sequence(pos: int, sequence: var SeqActive): bool =
        for s in sequence_lists:
            var found = true
            for i, ch in s.start.pairs:
                if text[pos+i] != ch:
                    found = false
                    break
            if found == false:
                continue
            sequence.sequence = s
            return true
        return false
    
    proc check_stop_sequence(pos: int, actseq: SeqActive): bool =
        if text[pos] in actseq.sequence.stop_extra:
            return true
        if pos > 0 and (text[pos-1] in actseq.sequence.negative_lookbehind):
            return false
        for i, s in actseq.sequence.stop.pairs:
            if text[pos+i] != s:
                return false
        return true
    
    template style_token(token_name: string, token_length: int) =
        if token_length > 0:
            if token_name in keywords:
                set_styling(token_length, styles["Keyword"])
                previous_token = token_name
            elif token_name in custom_keywords:
                set_styling(token_length, styles["CustomKeyword"])
            elif token_name[0].isdigit() or (token_name[0] == '.' and 
token_name[1].isdigit()):
                set_styling(token_length, styles["Number"])
            elif previous_token == "class":
                set_styling(token_length, styles["ClassName"])
                previous_token = ""
            elif previous_token == "def":
                set_styling(token_length, styles["FunctionMethodName"])
                previous_token = ""
            else:
                set_styling(token_length, styles["Default"])
    
    var i = 0
    token_start = i
    while i < text_length:
        if actseq.active == true or check_start_sequence(i, actseq) == true:
            #[
                Multiline sequence already started in the previous line or
                a start sequence was found
            ]#
            if actseq.active == false:
                # Style the currently accumulated token
                token_name = text[token_start..i]
                token_length = i - token_start
                style_token(token_name, token_length)
            # Set the states and reset the flags
            token_start = i
            token_length = 0
            if actseq.active == false:
                i += len(actseq.sequence.start)
            while i < text_length:
                # Check for end of comment
                if check_stop_sequence(i, actseq) == true:
                    i += len(actseq.sequence.stop)
                    break
                i += 1
            # Style text
            token_length = i - token_start
            set_styling(token_length, actseq.sequence.style)
            # Style the separator tokens after the sequence, if any
            token_start = i
            while text[i] in extended_separators and i < text_length:
                i += 1
            token_length = i - token_start
            if token_length > 0:
                set_styling(token_length, styles["Default"])
            # Set the states and reset the flags
            token_start = i
            token_length = 0
            # Skip to the next iteration, because the index is already
            # at the position at the end of the string
            actseq.active = false
            continue
        elif text[i] in extended_separators:
            #[
                Separator found
            ]#
            token_name = text[token_start..i-1]
            token_length = len(token_name)
            if token_length > 0:
                style_token(token_name, token_length)
                token_start = i
                while text[i] in extended_separators and i < text_length:
                    i += 1
                token_length = i - token_start
                if token_length > 0:
                    set_styling(token_length, styles["Default"])
                # State reset
                token_start = i
                token_length = 0
                continue
            else:
                while text[i] in extended_separators and i < text_length:
                    i += 1
                token_length = i - token_start
                if token_length > 0:
                    set_styling(token_length, styles["Default"])
                # State reset
                token_start = i
                token_length = 0
                continue
        # Update loop variables
        inc(i)
        # Check for end of text
        if i == text_length:
            token_name = text[token_start..i-1]
            token_length = len(token_name)
            style_token(token_name, token_length)
        elif i > text_length:
            raise newException(IndexError, "Styling went over the text length 
limit!")
    
#------------------------------------------------------------------------------
    clean_up()
    returnNone()
"""
)

# Lexer reset button
button = PyQt5.QtWidgets.QPushButton("RESET LEXER")
def reset_lexer(*args):
    nim_lexer = LexerNim(editor)
    editor.setLexer(nim_lexer)
button.clicked.connect(reset_lexer)

gb = PyQt5.QtWidgets.QGroupBox()
layout = PyQt5.QtWidgets.QVBoxLayout()
gb.setLayout(layout)
layout.addWidget(button)
layout.addWidget(editor)

# For the QScintilla editor to properly process events we need to add it to
# a QMainWindow object.
main_window = PyQt5.QtWidgets.QMainWindow()
# Set the central widget of the main window to be the editor
main_window.setCentralWidget(gb)
# Resize the main window and show it on the screen
main_window.resize(800, 600)
main_window.show()

# Execute the application
application.exec_()
_______________________________________________
QScintilla mailing list
[email protected]
https://www.riverbankcomputing.com/mailman/listinfo/qscintilla

Reply via email to