Solved! I ended up using a combination of Regex and good old programming logic.
The issue was the order in which I was colouring the different strings. If you colour different parts of a richtextbox in such a way that SelectionStart is not greater than the last, some parts lose their colour. That's what confused me - as there was no colour on certain parts I assumed the Regex hadn't captured things correctly.
To fix the ordering issue I stored the index and a SyntaxHighlightOperation holding the other necessary values for my highlight function in a SortedDictionary. That effectively sorted the highlighting operations by index, completely resolving the colouring issue.
Here's the result:
[
^]
And here's the final code:
Dim operations As New SortedDictionary(Of Integer, SyntaxHighlightOperation)
Dim code As String = codeEditorBox.Text
Dim anything As String = "[\(\);]"
Dim codeElements As String = "Server|Files"
Dim variableTypes As String = "string|int|bool|dec"
Dim enumerations As String = "SyntaxRating|Mood"
For Each match As Match In New Regex(anything).Matches(code)
operations.Add(match.Index, New SyntaxHighlightOperation(match.Index, 1, selPos, SyntaxColours.Symbol))
Next
For Each match As Match In New Regex(variableTypes).Matches(code)
If code.hasAnyAround(match.Value, match.Index, New String()() {
New String() {"""", """"},
New String() {"(", ")"}
}) Then
operations.Add(match.Index - 1, New SyntaxHighlightOperation(match.Index - 1, match.Value.Length + 2, selPos, SyntaxColours.Variable))
ElseIf code.hasAround(match.Value, match.Index, New String() {"List<", ">"}) AndAlso code.hasAround(match.Value, match.Index, New String() {"<", ">"}) Then
operations.Add(match.Index - 5, New SyntaxHighlightOperation(match.Index - 5, match.Value.Length + 2, selPos, SyntaxColours.List))
operations.Add(match.Index, New SyntaxHighlightOperation(match.Index, match.Value.Length, selPos, SyntaxColours.Variable))
operations.Add(match.Index + match.Value.Length, New SyntaxHighlightOperation(match.Index + match.Value.Length, 1, selPos, SyntaxColours.List))
End If
Next
For Each match As Match In New Regex(codeElements).Matches(code)
operations.Add(match.Index, New SyntaxHighlightOperation(match.Index, match.Value.Length, selPos, SyntaxColours.CodeElement))
Next
For Each match As Match In New Regex(enumerations).Matches(code)
If code.hasAfter(match.Value, match.Index, ".") Then
operations.Add(match.Index, New SyntaxHighlightOperation(match.Index, match.Value.Length, selPos, SyntaxColours.Enumeration))
End If
Next
For Each operation As SyntaxHighlightOperation In operations.Values
highlight(operation.getIndex(), operation.getLength(), operation.getSelPos(), operation.getColor())
Next
I also made an extensions class to help with the logic for requiring certain strings before, after or before and after the match:
Imports System.Runtime.CompilerServices
Module ExtensionMethods
<Extension()>
Public Function hasAround(code As String, text As String, index As Integer, around() As String) As Boolean
Dim before As String = around(0)
Dim after As String = around(1)
If index - before.Length < 0 OrElse index + text.Length = code.Length Then Return False
Return code.Substring(index - before.Length, before.Length) = before AndAlso code.Substring(index + text.Length, after.Length) = after
End Function
<Extension()>
Public Function hasAnyAround(code As String, text As String, index As Integer, around()() As String) As Boolean
For Each a As String() In around
Dim before As String = a(0)
Dim after As String = a(1)
If index - before.Length < 0 OrElse index + text.Length >= code.Length Then Return False
If code.Substring(index - before.Length, before.Length) = before AndAlso code.Substring(index + text.Length, after.Length) = after Then Return True
Next
Return False
End Function
<Extension()>
Public Function hasBefore(code As String, text As String, index As Integer, before As String) As Boolean
If index - before.Length < 0 Then Return False
Return code.Substring(index - before.Length, before.Length) = before
End Function
<Extension()>
Public Function hasAfter(code As String, text As String, index As Integer, after As String) As Boolean
If index + text.Length >= code.Length Then Return False
Return code.Substring(index + text.Length, after.Length) = after
End Function
End Module