SynEdit Highlighter
Understanding the SynEdit Highlighter
SynEdit - Highlighter relationship
SynEdit <-> Highlighter have a n to 1 relationship.
- 1 (instance of a) Highlighter can serve n (many) (instances of) SynEdits
- Each SynEdit only has one Highlighter
As a result of this:
- no Highlighter Instance has a (fixed) reference to the SynEdit.
- (Highlighters however keep a list of SynEditTextBuffers to which they are attached)
- All data for the Highlighter is (and must be) stored on the SynEdit (actually on the TextBuffer of SynEdit (referred to as "Lines").
However SynEdit ensures before each call to the Highlighter that Highlighter.CurrentLines is set to the current SynEdits Lines. This way the highlighter can access the data whenever needed. The Format of the data-storage is determined by the highlighter (TSynCustomHighlighter.AttachToLines)
Scanning and Returning Highlight attributes
The Highlighter is expected to work on a per Line base.
If any text was modified, SynEdit will call (TSynCustomHighlighter.ScanFrom / Currently called from TSynEdit.ScanFrom) with the line range. The Highlighter should know the state of the previous line.
If Highlight attributes are required SynEdit will request them per Line too. SynEdit will loop through individual tokens on a line. This currently happens from nested proc PaintLines in SynEdit.PaintTextLines. It calls TSynCustomHighlighter.StartAtLineIndex, followed by HL.GetTokenEx/HL.GetTokenAttribute for as long as HL.GetEol is false
Also the BaseClass for the Highlighter's data (see AttachToLines) is based on per line storage, and SynEdit's TextBuffer (Lines) do maintenance on this data to keep it synchronized. That is when ever lines of text are inserted or removed, so are entries inserted or removed from the highlighters data (hence it must have one entry per line).
Usually Highlighters store the end-of-line-status in this field. So if the highlighter is going to work on a line, it will continue with the state-entry from the previous line.
Folding
SynEdit's folding is handled by unit SynEditFoldedView and SynGutterCodeFolding. Highlighter that implement folding are to be based on TSynCustomFoldHighlighter
The basic information for communication between SynEditFoldedView and the HL requires 2 values stored for each line. (Of course the highlighter itself can store more information):
- FoldLevel at the end of line
- Minimum FoldLevel encountered anywhere on the line
The Foldlevel indicates how many (nested) folds exist. It goes up whenever a fold begins, and down when a fold ends
EndLvl MinLvl Procedure a; 1 - 0 Begin 2 -- 1 - b:= 1; 2 -- 2 -- if c > b then begin 3 --- 2 -- c:=b; 3 --- 3 --- end else begin 3 --- 2 -- b:=c; 3 --- 3 --- end; 2 -- 2 -- end; 0 0 // The end closes both: begin and procedure fold
In the line
Procedure a; 1 - 0
the MinLvl is 0, because the line started with a Level of 0 (and it never went down / no folds closed). Similar in all lines where there is only an opening fold keyword ("begin").
But the line
end else begin 3 --- 2 --
starts with a level of 3, and also ends with it (one close, one open). But since it went down first, the minimum level encountered anywhere on the line is 2.
Without the MinLvl it would not be possible to tell, that a fold ends in this line.
There is no such thing as a MaxLvl, because folds that start and end on the same line can not be folded anyway. No need to detect them.
if a then begin b:=1; c:=2; end; // no fold on that line
Creating a SynEdit Highlighter
This section is under construction
The Basics: Returning Tokens
The below highlighter implements the bare minimum. No pre-scan, no ranges, just some Attributes, and parsing each line into tokens
<delphi> interface
type
TSynDemoHl = class(TSynCustomHighlighter) private fCommentAttri: TSynHighlighterAttributes; fIdentifierAttri: TSynHighlighterAttributes; fSpaceAttri: TSynHighlighterAttributes; FTokenPos, FTokenEnd: Integer; FLineText: String; procedure FindTokenEnd; public procedure SetLine(const NewValue: String; LineNumber: Integer); override; procedure GetTokenEx(out TokenStart: PChar; out TokenLength: integer); override; function GetTokenAttribute: TSynHighlighterAttributes; override; procedure Next; override; function GetEol: Boolean; override; function GetDefaultAttribute(Index: integer): TSynHighlighterAttributes; override; constructor Create(AOwner: TComponent); override; published property CommentAttri: TSynHighlighterAttributes read fCommentAttri write fCommentAttri; property IdentifierAttri: TSynHighlighterAttributes read fIdentifierAttri write fIdentifierAttri; property SpaceAttri: TSynHighlighterAttributes read fSpaceAttri write fSpaceAttri; end;
implementation
procedure TSynDemoHl.FindTokenEnd; var
l: Integer;
begin
l := length(FLineText); FTokenEnd := FTokenPos; If FTokenPos > l then exit else if FLineText[FTokenEnd] in [#9, ' '] then while (FTokenEnd <= l) and (FLineText[FTokenEnd] in [#0..#32]) do inc (FTokenEnd) else while (FTokenEnd <= l) and not(FLineText[FTokenEnd] in [#9, ' ']) do inc (FTokenEnd)
end;
procedure TSynDemoHl.SetLine(const NewValue: String; LineNumber: Integer); begin
inherited; FTokenPos := 1; FLineText := NewValue; FindTokenEnd;
end;
procedure TSynDemoHl.GetTokenEx(out TokenStart: PChar; out TokenLength: integer); begin
TokenStart := @FLineText[FTokenPos]; TokenLength := FTokenEnd - FTokenPos;
end;
function TSynDemoHl.GetTokenAttribute: TSynHighlighterAttributes; begin
if FLineText[FTokenPos] in [#9, ' '] then Result := SpaceAttri else if LowerCase(copy(FLineText, FTokenPos, FTokenEnd-FTokenPos)) = 'comment' then Result := CommentAttri else Result := IdentifierAttri;
end;
procedure TSynDemoHl.Next; begin
FTokenPos := FTokenEnd; FindTokenEnd;
end;
function TSynDemoHl.GetEol: Boolean; begin
Result := FTokenPos > length(FLineText);
end;
function TSynDemoHl.GetDefaultAttribute(Index: integer): TSynHighlighterAttributes; begin
case Index of SYN_ATTR_COMMENT: Result := fCommentAttri; SYN_ATTR_IDENTIFIER: Result := fIdentifierAttri; SYN_ATTR_WHITESPACE: Result := fSpaceAttri; else Result := nil; end;
end;
constructor TSynDemoHl.Create(AOwner: TComponent); begin
inherited Create(AOwner); fCommentAttri := TSynHighlighterAttributes.Create('comment', 'comment'); AddAttribute(fCommentAttri);
fIdentifierAttri := TSynHighlighterAttributes.Create('ident', 'ident'); fIdentifierAttri.Style := [fsBold]; AddAttribute(fIdentifierAttri);
fSpaceAttri := TSynHighlighterAttributes.Create('space', 'space'); AddAttribute(fSpaceAttri);
end; </delphi>
<delphi> procedure Form1.Init; var
hl: TSynDemoHl;
begin
hl := TSynDemoHl.Create(Self); SynEdit1.Highlighter := hl; hl.CommentAttri.Foreground := clRed; hl.IdentifierAttri.Foreground := clGreen;
end; </delphi>
References
Threads on the forum:
http://www.lazarus.freepascal.org/index.php/topic,10260.0.html
http://www.lazarus.freepascal.org/index.php/topic,7879.0.html
http://www.lazarus.freepascal.org/index.php/topic,7338.0.html
http://www.lazarus.freepascal.org/index.php/topic,10959.msg54714
http://www.lazarus.freepascal.org/index.php/topic,11064
http://www.lazarus.freepascal.org/index.php/topic,11384.msg57160.html#msg57160 (obtaining highlight for printing)