aehimself 396 Posted November 26, 2021 Hello, I found and modified a code to highlight matching brackets in SynEdit in a Delphi IDE style (no color highlights, but a square around the characters. All is working fine except this one scenario: Enter this pattern and place the cursor in the middle of the inner brackets: Now, press backspace: And now, enter an opening bracket again: The rectangle around the first bracket doesn't get cleared and I can not figure out why. What is even more strange, if you put all in one line all works just fine: This image was taken after deleting and reentering the inner starting bracket. The code I have at the moment is the following: // This whole method was copied from https://stackoverflow.com/questions/18487553/synedit-onpainttransientdemo and modified // to draw rectangles instead of coloring. Procedure TEditorFrame.SynEdit1PaintTransient(Sender: TObject; Canvas: TCanvas; TransientType: TTransientType); Procedure PaintText(Const inPoint: TPoint; Const inText: String); Begin Case TransientType Of ttAfter: Begin // Clear brush and pen has font color: rectangle will be drawn SynEdit1.Canvas.Brush.Style := bsClear; SynEdit1.Canvas.Pen.Color := SynEdit1.Font.Color; End; ttBefore: Begin // Solid brush and pen has background color: Rectangle will be cleared SynEdit1.Canvas.Brush.Style := bsSolid; SynEdit1.Canvas.Pen.Color := SynEdit1.Color; End; End; SynEdit1.Canvas.Rectangle(inPoint.X, inPoint.Y, inPoint.X + SynEdit1.Canvas.TextWidth(inText), inPoint.Y + SynEdit1.Canvas.TextHeight(inText)); SynEdit1.Canvas.TextOut(inPoint.X, inPoint.Y, inText); End; Const OCSYMBOLS: Array[0..7] Of Char = ('(', ')', '{', '}', '[', ']', '<', '>'); Var bufcoord: TBufferCoord; tp: TPoint; a: Integer; c: Char; Begin bufcoord := SynEdit1.CaretXY; a := SynEdit1.RowColToCharIndex(bufcoord); If (a > 0) And (a <= SynEdit1.Text.Length) Then Begin // First, take the character BEHIND the cursor and check if it's an opener or a closer c := SynEdit1.Text[a]; If Not TArray.BinarySearch<Char>(OCSYMBOLS, c, a) Then c := #0; End Else c := #0; If c = #0 Then Begin // Variable a might have been overwritten by TArray.BinarySearch so let's initialize it again a := SynEdit1.RowColToCharIndex(bufcoord); If (a < SynEdit1.Text.Length) Then Begin // If the character behind the cursor wasn't an opener or a closer, // take the character AFTER the cursor and check the same c := SynEdit1.Text[a + 1]; If Not TArray.BinarySearch<Char>(OCSYMBOLS, c, a) Then c := #0; End; If c = #0 Then Exit; End Else bufcoord.Char := bufcoord.Char - 1; // Originally this was a FOR cycle but it's a waste to go through all if only one iteration actually performs something. // During the binary search we already got the index and if it's an opener or a closer. Simply use those previously saved // values SynEdit1.Canvas.Font.Assign(SynEdit1.Font); tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); PaintText(tp, c); bufcoord := SynEdit1.GetMatchingBracketEx(bufcoord); If (bufcoord.Char = 0) Or (bufcoord.Line = 0) Then Exit; tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); If tp.X <= SynEdit1.GutterWidth Then Exit; // We need to paint the opposite symbol now: if first one was an opener then a closer and vice versa. // Instead of an If statement we can simply use a XOr 1, as it will turn 1 -> 2 and 2 -> 1, 3 -> 4 and // 4 -> 3... giving the exact opposite symbol in the pair that we need PaintText(tp, OCSYMBOLS[a XOr 1]); End; I'm using TurboPack SynEdit on D10.4.2. If the code doesn't compile just use SynEdit1.Gutter.Width instead, I think nothing else had to be changed. Can someone please check and give some hints on why things go south? 🙂 Thanks! Share this post Link to post
Anders Melander 1784 Posted November 26, 2021 The version I use works. I don't have time to check the difference in how we do it but you can see for yourself if you want to: https://bitbucket.org/anders_melander/dwscriptstudio/src/15b205a8a7b4d461b786452aa4e0ee548779fe92/Source/amScriptDebuggerMain.pas#lines-1184 Share this post Link to post
pyscripter 689 Posted November 27, 2021 (edited) Anders is probably not using TurboPack SynEdit. His version of SynEdit is not handling ttBefore painting and the only way this can work is if SynEdit invalidates everything every time a character is typed. Turbopack SynEdit painting has been optimized to avoid flicker, so his version of Bracket Highlighting will not work with Turbopack SynEdit. I have committed some changes to Turbopack SynEdit (see Matching brackets highlighting with PaintTransient handler error · Issue #110 · TurboPack/SynEdit (github.com)). Could you please try with the latest version and see whether it now works correctly? Edited November 27, 2021 by pyscripter Share this post Link to post
pyscripter 689 Posted November 27, 2021 (edited) By the way in the code above: a := SynEdit1.RowColToCharIndex(bufcoord); followed by c := SynEdit1.Text[a]; in fact twice, is not efficient. You are better-off just using c := SynEdit1.Lines[bufcoord.Line][bufcoord.Char] Edited November 27, 2021 by pyscripter 1 Share this post Link to post
pyscripter 689 Posted November 27, 2021 Another issue with your code is that since you are using TArray.BinarySearch your bracket array needs to be sorted. But OCSYMBOLS: Array[0..7] Of Char = ('(', ')', '{', '}', '[', ']', '<', '>'); is not. 1 Share this post Link to post
pyscripter 689 Posted November 27, 2021 (edited) @Anders Melander You are using an old version of SynEdit. The key difference is the calls to UnionRect in TCustomSynEdit.InvalidateLines. Please compare to the SynEdit master branch. The difference is subtle, but in effect in your version any character entry invalidates the whole Window and this why you PaintTransient works. UnionRect(fInvalidateRect, fInvalidateRect, rcInval) is not the same as UnionRect(fInvalidateRect, rcInval, fInvalidateRect)! when fInvalidateRect is empty! Edited November 27, 2021 by pyscripter Share this post Link to post
aehimself 396 Posted November 27, 2021 @pyscripter The patch worked, it highlight correctly now! I saw that the request to be able to provide matching characters were also implemented so I changed the array to a string - this way no sorting is required and I can simply pass it to GetMatchingBracketEx. I'll take a look into the efficiency you mentioned. Thank you! Share this post Link to post
Anders Melander 1784 Posted November 27, 2021 4 hours ago, pyscripter said: You are using an old version of SynEdit. Yes. I'm not using that project at the moment so I haven't kept it up to date. Share this post Link to post
aehimself 396 Posted November 28, 2021 (edited) Unfortunately GetMatchingBracketEx does not support quotes - to be precise opener and closer pairs which are the same character. I wrote a quick handler for quotes, all is working like a charm: // This whole method was copied from https://stackoverflow.com/questions/18487553/synedit-onpainttransientdemo and modified // to draw rectangles instead of coloring. A few bugs were fixed and it was greatly simplified, too. Oh and while I figured // out what it is doind exactly, I placed some comments for easier understanding :) Procedure TEditorFrame.SynEdit1PaintTransient(Sender: TObject; Canvas: TCanvas; TransientType: TTransientType); Procedure PaintText(Const inPoint: TPoint; Const inText: String); Begin Case TransientType Of ttAfter: Begin // Clear brush and pen has font color: rectangle will be drawn SynEdit1.Canvas.Brush.Style := bsClear; SynEdit1.Canvas.Pen.Color := SynEdit1.Font.Color; End; ttBefore: Begin // Solid brush and pen has background color: Rectangle will be cleared SynEdit1.Canvas.Brush.Style := bsSolid; SynEdit1.Canvas.Pen.Color := SynEdit1.Color; End; End; SynEdit1.Canvas.Rectangle(inPoint.X, inPoint.Y, inPoint.X + SynEdit1.Canvas.TextWidth(inText), inPoint.Y + SynEdit1.Canvas.TextHeight(inText)); SynEdit1.Canvas.TextOut(inPoint.X, inPoint.Y, inText); End; Const OCSYMBOLS = '()[]{}<>''"'; Var bufcoord: TBufferCoord; tp: TPoint; index, a: Integer; c: Char; Begin c := #0; bufcoord := SynEdit1.CaretXY; If (bufcoord.Char = 0) Or (bufcoord.Line = 0) Then Exit; If bufcoord.Char > 1 Then Begin // Take the character BEHIND the cursor and check if it's a character we need to highlight c := SynEdit1.Lines[bufcoord.Line - 1][bufcoord.Char - 1]; index := OCSYMBOLS.IndexOf(c); End Else index := -1; If index = -1 Then If bufcoord.Char > Length(SynEdit1.Lines[bufcoord.Line - 1]) Then Exit Else Begin // The character behind the cursor wasn't something we need to highlight. // Take the character AFTER the cursor and check the same c := SynEdit1.Lines[bufcoord.Line - 1][bufcoord.Char]; index := OCSYMBOLS.IndexOf(c); If index = -1 Then Exit; End Else bufcoord.Char := bufcoord.Char - 1; // Originally this was a FOR cycle but it's a waste to go through all if only one iteration actually performs something. // During the search we already got the character which requires highlighting. Simply use that to save some cycles SynEdit1.Canvas.Font.Assign(SynEdit1.Font); tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); PaintText(tp, c); Case c Of '''', '"': Begin // GetMatchingBracketEx does not support the same opening and closing characters. Therefore quotes // need special handling. // This quick and easy method only detects quotes in the same line. index := bufcoord.Char; a := index - 1; // Try to find the previous quote of the same type While a > 0 Do Begin If SynEdit1.Lines[bufcoord.Line - 1][a] = c Then Break; Dec(a); End; If a = 0 Then Begin // No previous quote was found. Reset the position and try to find the next one a := index + 1; While a < Length(SynEdit1.Lines[bufcoord.Line - 1]) Do Begin If SynEdit1.Lines[bufcoord.Line - 1][a] = c Then Break; Inc(a); End; If a > Length(SynEdit1.Lines[bufcoord.Line - 1]) Then Exit; End; bufcoord.Char := a; tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); End; Else Begin bufcoord := SynEdit1.GetMatchingBracketEx(bufcoord, OCSYMBOLS); If (bufcoord.Char = 0) Or (bufcoord.Line = 0) Then Exit; tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); If tp.X <= SynEdit1.GutterWidth Then Exit; // We need to paint the opposite symbol now: if first one was an opener then a closer and vice versa. // Instead of an If statement we can simply use a XOr 1, as it will turn 1 -> 2 and 2 -> 1, 3 -> 4 and // 4 -> 3... giving the exact opposite symbol in the pair that we need c := OCSYMBOLS.Chars[index XOr 1]; End; End; PaintText(tp, c); End; Feel free to use it if you like; any comment or improvement is welcome 🙂 Edited November 28, 2021 by aehimself Share this post Link to post
aehimself 396 Posted December 1, 2021 I ended up changing the quote detection as it misbehaved in several occasions. At the moment it looks like this: '''', '"': Begin // GetMatchingBracketEx does not support the same opening and closing characters. Therefore quotes // need special handling. // This quick and easy method only detects quotes in the same line. index := bufcoord.Char; If bufcoord.Char > 1 Then Begin // Check the current token BEFORE the current quote. If it's a String or a delimited identifier, // the currently selected is the closer. Go backwards to find the opener. Dec(bufcoord.Char); If Not SynEdit1.GetHighlighterAttriAtRowCol(bufcoord, token, attr) Or ((attr <> SynHighlight.StringAttribute) And (attr <> SynHighlight.DelimitedIdentifierAttri)) Or Not token.StartsWith(c) Then a := 0 Else Begin a := Length(token); Dec(bufcoord.Char, a - 2); End; End Else a := 0; If (a = 0) And (bufcoord.Char < Length(SynEdit1.Lines[bufcoord.Line - 1])) Then Begin // Character before the current quote was not the correct token or opener quote was not found. // Attempt to do the same check with the character after, this time looking for a closer... bufcoord.Char := index + 1; If Not SynEdit1.GetHighlighterAttriAtRowCol(bufcoord, token, attr) Or ((attr <> SynHighlight.StringAttribute) And (attr <> SynHighlight.DelimitedIdentifierAttri)) Or Not Token.EndsWith(c) Then Exit Else Begin a := Length(token); Inc(bufcoord.Char, a - 2); End; End; tp := SynEdit1.RowColumnToPixels(SynEdit1.BufferToDisplayPos(bufcoord)); End; This way I can correctly determine if the token is before or after the current character, which direction I should look for the closer. It works, with only 1 limitation: the token returned is only the part of the token, which is the current line - therefore highlighting quotes only work in one line as well. Is there a way to make GetHighlighterAttriAtRowCol return the full token (including line breaks) or an other method I can use? Share this post Link to post