Skip to content

Character Autocompletion

Brandon Desjarlais edited this page Dec 6, 2023 · 1 revision

This topic demonstrates the autocompletion of () {} [] '' "". It is certainly easily achievable by using the CharAdded event, which is triggered when a character is inserted.

You can skip to Finishing Touch for full code.

Implementation

private void scintilla_CharAdded(object sender, CharAddedEventArgs e)
{
    switch (e.Char)
    {
        case '(':
            scintilla.InsertText(scintilla.CurrentPosition, ")");
            break;
        case '{':
            scintilla.InsertText(scintilla.CurrentPosition, "}");
            break;
        case '[':
            scintilla.InsertText(scintilla.CurrentPosition, "]");
            break;
        case '"':
            scintilla.InsertText(scintilla.CurrentPosition, "\"");
            break;
        case '\'':
            scintilla.InsertText(scintilla.CurrentPosition, "'");
            break;
    }
}

That is basically it. As simple as it gets. However, the code above is perhaps great only if you do not intend to autocomplete '' and "". The reason is that '' and "" will embarrassingly always autocomplete no matter where they are inserted. For example, let say you are going to write "you're" while having autocompletion for ''. Your output will be "you''re". Oops! You sure don't want that, do you?

Workaround

The code above needs to be further developed to become smarter and more efficient. Let's start with the variables we're going to declare:

// int; the current position of caret
var caretPos = scintilla.CurrentPosition;

// bool; the caret is at the very beginning of the document
var docStart = caretPos == 1;

// bool; the caret is at the very end of the document
var docEnd = caretPos == scintilla.Text.Length;
// int; gets what's before the inserted character. Notice the ternary operator.
// If the caret is at the beginning of the document, Scintilla will not check
// what's behind it (because there's nothing)
var charPrev = docStart ? scintilla.GetCharAt(caretPos) : scintilla.GetCharAt(caretPos - 2);

// int; gets what's after the inserted character
var charNext = scintilla.GetCharAt(caretPos);
// bool; checks if what's behind the inserted character is a space, tab,
// new line, or a carriage return. They are "flags" that will be used to
// allow the autocompletion of the '' or ""
var isCharPrevBlank = charPrev == ' '  || charPrev == '\t' || charPrev == '\n' || charPrev == '\r';

// bool; same as above, but for what's after the inserted character.
// docEnd will allow the autocompletion after the caret has reached
// the end of the document
var isCharNextBlank = charNext == ' '  || charNext == '\t' || charNext == '\n' || charNext == '\r' || docEnd;
// bool; checks if the inserted character is enclosed. ie: ['']
var isEnclosed = (charPrev == '(' && charNext == ')') ||
                 (charPrev == '{' && charNext == '}') ||
                 (charPrev == '[' && charNext == ']');

// bool; checks if the inserted character is enclosed, but there's a
// space before or after it. ie: ['' ][ '']
var isSpaceEnclosed = (charPrev == '(' && isCharNextBlank) || (isCharPrevBlank && charNext == ')') ||
                      (charPrev == '{' && isCharNextBlank) || (isCharPrevBlank && charNext == '}') ||
                      (charPrev == '[' && isCharNextBlank) || (isCharPrevBlank && charNext == ']');
// bool; combination of all of the flags above
var isCharOrString  = (isCharPrevBlank && isCharNextBlank) || isEnclosed || isSpaceEnclosed;

And now, the switch statement:

switch (e.Char)
{
    case '"':
        /* Improve the behavior of the autocompletion so that the caret
         * does not remain in the center upon multiple character insertions
         * This is how it will work:
         * 
         * | is a caret line.
         * => is output
         * 
         * insert ' => '|'
         * insert ' again => ''|
         * insert ' again => '''|
         * and so on...
         *
         * This is the same behavior observed in VS Code, Sublime Text and
         * Notepad++.
        */
        if (charPrev == 0x22 && charNext == 0x22)
        {
            scintilla.DeleteRange(caretPos, 1);
            scintilla.GotoPosition(caretPos);
            return;
        }
        // Make sure all the flags apply
        if (isCharOrString)
            scintilla.InsertText(scintilla.CurrentPosition, "\"");
        break;
}

Finishing Touch

Let's put all of the above in one method to keep the CharAdded event clean and tidy.

private void InsertMatchedChars(CharAddedEventArgs e)
{
    var caretPos = scintilla.CurrentPosition;
    var docStart = caretPos == 1;
    var docEnd   = caretPos == scintilla.Text.Length;

    var charPrev = docStart ? scintilla.GetCharAt(caretPos) : scintilla.GetCharAt(caretPos - 2);
    var charNext = scintilla.GetCharAt(caretPos);

    var isCharPrevBlank = charPrev == ' '  || charPrev == '\t' ||
                          charPrev == '\n' || charPrev == '\r';

    var isCharNextBlank = charNext == ' '  || charNext == '\t' ||
                          charNext == '\n' || charNext == '\r' ||
                          docEnd;

    var isEnclosed      = (charPrev == '(' && charNext == ')') ||
                          (charPrev == '{' && charNext == '}') ||
                          (charPrev == '[' && charNext == ']');

    var isSpaceEnclosed = (charPrev == '(' && isCharNextBlank) || (isCharPrevBlank && charNext == ')') ||
                          (charPrev == '{' && isCharNextBlank) || (isCharPrevBlank && charNext == '}') ||
                          (charPrev == '[' && isCharNextBlank) || (isCharPrevBlank && charNext == ']');

    var isCharOrString  = (isCharPrevBlank && isCharNextBlank) || isEnclosed || isSpaceEnclosed;

    var charNextIsCharOrString = charNext == '"' || charNext == '\'';

    switch (e.Char)
    {
        case '(':
            if (charNextIsCharOrString) return;
            scintilla.InsertText(caretPos, ")");
            break;
        case '{':
            if (charNextIsCharOrString) return;
            scintilla.InsertText(caretPos, "}");
            break;
        case '[':
            if (charNextIsCharOrString) return;
            scintilla.InsertText(caretPos, "]");
            break;
        case '"':
            // 0x22 = "
            if (charPrev == 0x22 && charNext == 0x22)
            {
                scintilla.DeleteRange(caretPos, 1);
                scintilla.GotoPosition(caretPos);
                return;
            }

            if (isCharOrString)
                scintilla.InsertText(caretPos, "\"");
            break;
        case '\'':
            // 0x27 = '
            if (charPrev == 0x27 && charNext == 0x27)
            {
                scintilla.DeleteRange(caretPos, 1);
                scintilla.GotoPosition(caretPos);
                return;
            }

            if (isCharOrString)
                scintilla.InsertText(caretPos, "'");
            break;
    }
}
private void scintilla_CharAdded(object sender, CharAddedEventArgs e)
{
    InsertMatchedChars(e);
}

That all, folks! '' and "" are now much smarter and will not trigger between text.

Visual Basic version of C# code above

Note: The Scintilla control is called txtScript.

    Private Sub InsertMatchedChars(charNew As Char)
        Dim caretPos As Integer = txtScript.CurrentPosition
        Dim docStart As Boolean = caretPos = 1
        Dim docEnd As Boolean = caretPos = txtScript.Text.Length

        Dim charPrev As Char = If(docStart, ChrW(txtScript.GetCharAt(caretPos)), ChrW(txtScript.GetCharAt(caretPos - 2)))
        Dim charNext As Char = ChrW(txtScript.GetCharAt(caretPos))

        Dim isCharPrevBlank As Boolean = charPrev = " " OrElse charPrev = "\t" OrElse
                          charPrev = "\n" OrElse charPrev = "\r"

        Dim isCharNextBlank As Boolean = charNext = " " OrElse charNext = "\t" OrElse
                          charNext = "\n" OrElse charNext = "\r" OrElse
                          docEnd

        Dim isEnclosed As Boolean = (charPrev = "(" AndAlso charNext = ")") OrElse
                          (charPrev = "{" AndAlso charNext = "}") OrElse
                          (charPrev = "[" AndAlso charNext = "]")

        Dim isSpaceEnclosed As Boolean = (charPrev = "(" AndAlso isCharNextBlank) OrElse (isCharPrevBlank AndAlso charNext = ")") OrElse
                          (charPrev = "{" AndAlso isCharNextBlank) OrElse (isCharPrevBlank AndAlso charNext = "}") OrElse
                          (charPrev = "[" AndAlso isCharNextBlank) OrElse (isCharPrevBlank AndAlso charNext = "]")

        Dim isCharOrString As Boolean = (isCharPrevBlank AndAlso isCharNextBlank) OrElse isEnclosed OrElse isSpaceEnclosed

        Dim charNextIsCharOrString As Boolean = charNext = """" OrElse charNext = "'"

        Select Case charNew
            Case "("
                If charNextIsCharOrString Then
                    Exit Sub
                End If
                txtScript.InsertText(caretPos, ")")
            Case "{"
                If charNextIsCharOrString Then
                    Exit Sub
                End If
                txtScript.InsertText(caretPos, "}")
            Case "["
                If charNextIsCharOrString Then
                    Exit Sub
                End If
                txtScript.InsertText(caretPos, "]")
            Case """"
                If charPrev = """" AndAlso charNext = """" Then
                    txtScript.DeleteRange(caretPos, 1)
                    txtScript.GotoPosition(caretPos)
                    Exit Sub
                End If
                If isCharOrString Then
                    txtScript.InsertText(caretPos, """")
                End If
            Case "'"
                If charPrev = "'" AndAlso charNext = "'" Then
                    txtScript.DeleteRange(caretPos, 1)
                    txtScript.GotoPosition(caretPos)
                End If
                If isCharOrString Then
                    txtScript.InsertText(caretPos, "'")
                End If
        End Select
    End Sub
    Private Sub txtScript_CharAdded(sender As Object, e As CharAddedEventArgs) Handles txtScript.CharAdded
        InsertMatchedChars(ChrW(e.Char))
    End Sub