pythonpython-3.xpyqtpyqt5qscintilla

QScintilla based text editor in PyQt5 with clickable functions and variables


I am trying to make a simple texteditor with basic syntax highlighting, code completion and clickable functions & variables in PyQt5. My best hope to achieve this is using the QScintilla port
for PyQt5.
I have found the following QScintilla-based texteditor example on the Eli Bendersky website (http://eli.thegreenplace.net/2011/04/01/sample-using-qscintilla-with-pyqt, Victor S. has adapted it to PyQt5). I think this example is a good starting point:

#-------------------------------------------------------------------------
# qsci_simple_pythoneditor.pyw
#
# QScintilla sample with PyQt
#
# Eli Bendersky (eliben@gmail.com)
# This code is in the public domain
#-------------------------------------------------------------------------
import sys

import sip
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.Qsci import QsciScintilla, QsciLexerPython


class SimplePythonEditor(QsciScintilla):
    ARROW_MARKER_NUM = 8

    def __init__(self, parent=None):
        super(SimplePythonEditor, self).__init__(parent)

        # Set the default font
        font = QFont()
        font.setFamily('Courier')
        font.setFixedPitch(True)
        font.setPointSize(10)
        self.setFont(font)
        self.setMarginsFont(font)

        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(font)
        self.setMarginsFont(font)
        self.setMarginWidth(0, fontmetrics.width("00000") + 6)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#cccccc"))

        # Clickable margin 1 for showing markers
        self.setMarginSensitivity(1, True)
#        self.connect(self,
#            SIGNAL('marginClicked(int, int, Qt::KeyboardModifiers)'),
#            self.on_margin_clicked)
        self.markerDefine(QsciScintilla.RightArrow,
            self.ARROW_MARKER_NUM)
        self.setMarkerBackgroundColor(QColor("#ee1111"),
            self.ARROW_MARKER_NUM)

        # Brace matching: enable for a brace immediately before or after
        # the current position
        #
        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)

        # Current line visible with special background color
        self.setCaretLineVisible(True)
        self.setCaretLineBackgroundColor(QColor("#ffe4e4"))

        # Set Python lexer
        # Set style for Python comments (style number 1) to a fixed-width
        # courier.
        #

        lexer = QsciLexerPython()
        lexer.setDefaultFont(font)
        self.setLexer(lexer)

        text = bytearray(str.encode("Arial"))
# 32, "Courier New"         
        self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, text)

        # Don't want to see the horizontal scrollbar at all
        # Use raw message to Scintilla here (all messages are documented
        # here: http://www.scintilla.org/ScintillaDoc.html)
        self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)

        # not too small
        self.setMinimumSize(600, 450)

    def on_margin_clicked(self, nmargin, nline, modifiers):
        # Toggle marker for the line the margin was clicked on
        if self.markersAtLine(nline) != 0:
            self.markerDelete(nline, self.ARROW_MARKER_NUM)
        else:
            self.markerAdd(nline, self.ARROW_MARKER_NUM)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    editor = SimplePythonEditor()
    editor.show()
    editor.setText(open(sys.argv[0]).read())
    app.exec_()

Just copy-paste this code into an empty .py file, and run it. You should get the following simple texteditor appearing on your display:

enter image description here

Notice how perfect the syntax highlighting is! QScintilla certainly did some parsing on the background to achieve that.
Is it possible to make clickable functions & variables for this texteditor? Every self-respecting IDE has it. You click on a function, and the IDE jumps to the function definition. The same for variables. I would like to know:


EDIT :
λuser noted the following:

Clickable function names require full parsing with a much deeper knowledge of a programming language [..]
This is way beyond the scope of Scintilla/QScintilla. Scintilla provides a way to react when the mouse clicks somewhere on the text, but the logic of "where is the definition of a function" is not in Scintilla and probably never will be.
However, some projects are dedicated to this task, like ctags. You could simply write a wrapper around this kind of tool.

I guess that writing such wrapper for ctags is now on my TODO list. The very first step is to get a reaction (Qt signal) when the user clicks on a function or variable. And perhaps the function/variable should turn a bit blueish when you hover with the mouse over it, to notify the user that it is clickable. I already tried to achieve this, but am held back by the shortage of QScintilla documentation.

So let us trim down the question to: How do you make a function or variable in the QScintilla texteditor clickable (with clickable defined as 'something happens')


EDIT :
I just returned to this question now - several months later. I have been cooperating with my friend Matic Kukovec to design a website about QScintilla. It is a beginner-friendly tutorial on how to use it:

enter image description here

https://qscintilla.com/

I hope this initiative fills the gap of lacking documentation.


Solution

  • A way to use Pyqt5 with option with clickable functions and variables. Your script that have the clickable part Quoted out, would look like this in PyQt5 with a custom signal.

    PyQt4 SIGNAL

    self.connect(self,SIGNAL('marginClicked(int, int, Qt::KeyboardModifiers)'), 
    self.on_margin_clicked)
    

    PyQt5 SIGNAL

    self.marginClicked.connect(self.on_margin_clicked)
    

    PyQt5

    import sys
    
    import sip
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.Qsci import QsciScintilla, QsciLexerPython
    
    
    class SimplePythonEditor(QsciScintilla):
        ARROW_MARKER_NUM = 8
    
        def __init__(self, parent=None):
            super(SimplePythonEditor, self).__init__(parent)
    
            # Set the default font
            font = QFont()
            font.setFamily('Courier')
            font.setFixedPitch(True)
            font.setPointSize(10)
            self.setFont(font)
            self.setMarginsFont(font)
    
            # Margin 0 is used for line numbers
            fontmetrics = QFontMetrics(font)
            self.setMarginsFont(font)
            self.setMarginWidth(0, fontmetrics.width("00000") + 6)
            self.setMarginLineNumbers(0, True)
            self.setMarginsBackgroundColor(QColor("#cccccc"))
    
            # Clickable margin 1 for showing markers
            self.setMarginSensitivity(1, True)
            self.marginClicked.connect(self.on_margin_clicked)
            self.markerDefine(QsciScintilla.RightArrow,
                self.ARROW_MARKER_NUM)
            self.setMarkerBackgroundColor(QColor("#ee1111"),
                self.ARROW_MARKER_NUM)
    
            # Brace matching: enable for a brace immediately before or after
            # the current position
            #
            self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
    
            # Current line visible with special background color
            self.setCaretLineVisible(True)
            self.setCaretLineBackgroundColor(QColor("#ffe4e4"))
    
            # Set Python lexer
            # Set style for Python comments (style number 1) to a fixed-width
            # courier.
            #
    
            lexer = QsciLexerPython()
            lexer.setDefaultFont(font)
            self.setLexer(lexer)
    
            text = bytearray(str.encode("Arial"))
    # 32, "Courier New"
            self.SendScintilla(QsciScintilla.SCI_STYLESETFONT, 1, text)
    
            # Don't want to see the horizontal scrollbar at all
            # Use raw message to Scintilla here (all messages are documented
            # here: http://www.scintilla.org/ScintillaDoc.html)
            self.SendScintilla(QsciScintilla.SCI_SETHSCROLLBAR, 0)
    
            # not too small
            self.setMinimumSize(600, 450)
    
        def on_margin_clicked(self, nmargin, nline, modifiers):
            # Toggle marker for the line the margin was clicked on
            if self.markersAtLine(nline) != 0:
                self.markerDelete(nline, self.ARROW_MARKER_NUM)
            else:
                self.markerAdd(nline, self.ARROW_MARKER_NUM)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        editor = SimplePythonEditor()
        editor.show()
        editor.setText(open(sys.argv[0]).read())
        app.exec_()