-
Notifications
You must be signed in to change notification settings - Fork 28
Character Autocompletion
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.
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?
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;
}
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.
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