How can I make QScintilla auto-indent like SublimeText?
Asked Answered
F

2

19

Consider the below mcve:

import sys
import textwrap

from PyQt5.Qsci import QsciScintilla
from PyQt5.Qt import *


if __name__ == '__main__':

    app = QApplication(sys.argv)
    view = QsciScintilla()

    view.SendScintilla(view.SCI_SETMULTIPLESELECTION, True)
    view.SendScintilla(view.SCI_SETMULTIPASTE, 1)
    view.SendScintilla(view.SCI_SETADDITIONALSELECTIONTYPING, True)

    view.setAutoIndent(True)
    view.setTabWidth(4)
    view.setIndentationGuides(True)
    view.setIndentationsUseTabs(False)
    view.setBackspaceUnindents(True)

    view.setText(textwrap.dedent("""\
        def foo(a,b):
            print('hello')
    """))

    view.show()
    app.exec_()

The behaviour of the auto-indent of the above snippet is really bad when comparing it with editors such as SublimeText or CodeMirror. First let's see how nice will behave the autoindent feature in SublimeText with single or multiple selections.

enter image description here

And now let's see how the auto-indent works in with the above snippet:

enter image description here

In comparison to SublimeText the way QScintilla works when auto-indentation is enabled with both single/multi selections is corky and really bad/unusable.

The first step to make the widget more like SublimeText/Codemirror would be disconnecting the current slot that makes autoindentation to behave badly, we can achieve that by doing:

print(view.receivers(view.SCN_CHARADDED))
view.SCN_CHARADDED.disconnect()
print(view.receivers(view.SCN_CHARADDED))

At this point you'd be ready to connect SCN_CHARADDED with your custom slot doing all the magic :)

QUESTION: How would you modify the above snippet so all selections will be preserved and the auto-indentation will behave exactly like SublimeText, Codemirror or any serious text editor out there?

REFERENCES:

qsciscintilla.h

class QSCINTILLA_EXPORT QsciScintilla : public QsciScintillaBase
{
    Q_OBJECT

public:
    ...
    private slots:
        void handleCharAdded(int charadded);
    ...
    private:
        void autoIndentation(char ch, long pos);

qsciscintilla.cpp

connect(this,SIGNAL(SCN_CHARADDED(int)),
         SLOT(handleCharAdded(int)));

...

// Handle the addition of a character.
void QsciScintilla::handleCharAdded(int ch)
{
    // Ignore if there is a selection.
    long pos = SendScintilla(SCI_GETSELECTIONSTART);

    if (pos != SendScintilla(SCI_GETSELECTIONEND) || pos == 0)
        return;

    // If auto-completion is already active then see if this character is a
    // start character.  If it is then create a new list which will be a subset
    // of the current one.  The case where it isn't a start character seems to
    // be handled correctly elsewhere.
    if (isListActive() && isStartChar(ch))
    {
        cancelList();
        startAutoCompletion(acSource, false, use_single == AcusAlways);

        return;
    }

    // Handle call tips.
    if (call_tips_style != CallTipsNone && !lex.isNull() && strchr("(),", ch) != NULL)
        callTip();

    // Handle auto-indentation.
    if (autoInd)
    {
        if (lex.isNull() || (lex->autoIndentStyle() & AiMaintain))
            maintainIndentation(ch, pos);
        else
            autoIndentation(ch, pos);
    }

    // See if we might want to start auto-completion.
    if (!isCallTipActive() && acSource != AcsNone)
    {
        if (isStartChar(ch))
            startAutoCompletion(acSource, false, use_single == AcusAlways);
        else if (acThresh >= 1 && isWordCharacter(ch))
            startAutoCompletion(acSource, true, use_single == AcusAlways);
    }
}

IMPORTANT: I've decided to post the relevant c++ bits so you'll got more background about how the indentation is achieved internally to give more clues about a possible replacement... The goal of this thread is to try to find a pure python solution though. I'd like to avoid modifying the QScintilla source code (if possible) so maintenance/upgrading will remain as simple as possible and QScintilla dep can still be seen as a black box.

Funch answered 24/4, 2019 at 9:2 Comment(13)
This is a C++ question as well as a Python question, maybe it would help to add the C++ tag.Tartaglia
@LogicalBranch Mmmm, you've got a point there... thing is, I'd like first to know whether there is a pure python solution that solves the given problem. Why? Well, at the moment we decide to modify the internal QScintilla source code (owned by riverbank) we won't be able to upgrade version through pypi easily anymore... also, it wouldn't be that straightforward to recompile for all major platforms. Reason why I've posted the internal c++ bits was to give relevant info to potential contributors. Does it make sense?Funch
As python solution you'd need an observer, don't know if you can hook it in somehow. Another solution would be a makro for QScintilla - just brainstorming, it's an interesting question.Raychel
I know it's kind of hacky but since you're using multiple cursors, when more than one cursor is selected, can't you store the location/position of the cursors in a temporary variable? Then when a white space character is inserted, restore the cursors to their original positions letting the user carry on typing in those positions.Tartaglia
Hey guys, I've been playing with notepad++ and it works quite ok, this editor is based on Scintilla... For those who don't know, QScintilla is also using Scintilla behind the curtains. Although notepad++ doesn't support multiselection (at least by default)... Anyway, the brainstorming you're doing is cool, keep it going... Not sure if there is some way to disconnect the private slot somehow and hook our own functionality in some hacky way :/ ...Funch
There is some slot for extensions too, isn't it?Raychel
That's a good question, here's a sorted list of QSciScintilla attributes, check the ones starting by SCN_... maybe... QSciScintilla inherits from QsciScintillaBase, maybe there is someone that we could use, dunno :(Funch
Cool, it seems the Signals are docummented, https://www.riverbankcomputing.com/static/Docs/QScintilla/classQsciScintillaBase.html#signalsFunch
Yeah I thought already my hint about observer was so far useless as it's existing already somehow with the signalsRaychel
Made a little experiment... edited my question, check it outFunch
I don't think that you've to get rid of the private slot, probably it's even important to have it.Raychel
@Raychel Edited again... when i use disconnect the number of receivers will become 0 but you can still edit stuff normallyFunch
Actually... i think disconnect got rid of the connection with the private slot... \:O/ , so this is become a matter of using our custom hook, gonna edit the whole question again to make it more clear nowFunch
R
1

It looks like you have to code your own version, the documentation mentions the most important point's about it already in the chapter Installation:

As supplied QScintilla will be built as a shared library/DLL and installed in the same directories as the Qt libraries and include files.

If you wish to build a static version of the library then pass CONFIG+=staticlib on the qmake command line.

If you want to make more significant changes to the configuration then edit the file qscintilla.pro in the Qt4Qt5 directory.

If you do make changes, specifically to the names of the installation directories or the name of the library, then you may also need to update the Qt4Qt5/features/qscintilla2.prf file.*

Further steps are explained there too.

Raychel answered 27/4, 2019 at 14:54 Comment(0)
B
0

There is no integrated way to make auto-indentation work in QScintilla (especially as it is in SublimeText). This behaviour is a language-specific and user-specific. Native Scintilla documentation includes examples of how to trigger auto-indentation. Sorry but it's written in C#. I haven't found it written in Python.

Here's a code (I know that a QScintilla is a Port to Qt, this Scintilla-oriented code should work with QScintilla too, or at the worst you can adapt it for C++):

private void Scintilla_InsertCheck(object sender, InsertCheckEventArgs e) {

    if ((e.Text.EndsWith("" + Constants.vbCr) || e.Text.EndsWith("" + Constants.vbLf))) {
        int startPos = Scintilla.Lines(Scintilla.LineFromPosition(Scintilla.CurrentPosition)).Position;
        int endPos = e.Position;
        string curLineText = Scintilla.GetTextRange(startPos, (endPos - startPos)); 
        // Text until the caret so that the whitespace is always
        // equal in every line.

        Match indent = Regex.Match(curLineText, "^[ \\t]*");
        e.Text = (e.Text + indent.Value);
        if (Regex.IsMatch(curLineText, "{\\s*$")) {
            e.Text = (e.Text + Constants.vbTab);
        }
    }
}

private void Scintilla_CharAdded(object sender, CharAddedEventArgs e) {

    //The '}' char.
    if (e.Char == 125) {
        int curLine = Scintilla.LineFromPosition(Scintilla.CurrentPosition);

        if (Scintilla.Lines(curLine).Text.Trim() == "}") { 
        //Check whether the bracket is the only thing on the line. 
        //For cases like "if() { }".
            SetIndent(Scintilla, curLine, GetIndent(Scintilla, curLine) - 4);
        }
    }
}

//Codes for the handling the Indention of the lines.
//They are manually added here until they get officially 
//added to the Scintilla control.

#region "CodeIndent Handlers"
    const int SCI_SETLINEINDENTATION = 2126;
    const int SCI_GETLINEINDENTATION = 2127;
    private void SetIndent(ScintillaNET.Scintilla scin, int line, int indent) {
        scin.DirectMessage(SCI_SETLINEINDENTATION, new IntPtr(line), new IntPtr(indent));
    }
    private int GetIndent(ScintillaNET.Scintilla scin, int line) {
        return (scin.DirectMessage(SCI_GETLINEINDENTATION, new IntPtr(line), null).ToInt32);
    }
#endregion

Hope this helps.

Bookmobile answered 2/5, 2019 at 18:0 Comment(2)
Hi, first of all, thanks for trying, not sure I'll have enough time to test that code before the bounty expires... But let me ask you, if I'm able to port that to python the autoindentation would behave like SublimeText (even when dealing with multiple selections)? Also, you said in your answer there is no integrated way to make auto-indentation, make sure you read again my question to understand what's going on as my whole point was setAutoIndent won't behave like Sublime. In any case, where is this code being used (any windows exe i can play with?)Funch
My colleague said it's a ScintillaNET code and he tested it in Windows. Honestly, I'm not 100% sure it's working with multiple selections. But my colleague said it is.Bookmobile

© 2022 - 2024 — McMap. All rights reserved.