Jump to content
aehimself

SynEdit bracket highlight

Recommended Posts

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:

image.png.b67f4f4d96ef16e495f361814f47518d.png

 

Now, press backspace:

image.png.e28cbfb9d6929cdca86130cdafc9212d.png

 

And now, enter an opening bracket again:

image.png.4cad3d4bffb0c5c23fc00699268ff1c3.png

 

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:

image.png.939a4e657616f212c961c1213af9bbb2.png

 

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 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 by pyscripter

Share this post


Link to post

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 by pyscripter
  • Like 1

Share this post


Link to post

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.

  • Like 1

Share this post


Link to post

@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 by pyscripter

Share this post


Link to post

@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
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

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 by aehimself

Share this post


Link to post

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

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×