From eefa1a85590f55af648d86b83e6b178ba0496f53 Mon Sep 17 00:00:00 2001 From: martin Date: Tue, 29 Jan 2013 18:21:04 +0000 Subject: [PATCH] SynEdit: added new markup. TSynEditMarkupHighlightAllMulti. Also refactor/speed up git-svn-id: trunk@40028 - --- .gitattributes | 1 + components/synedit/allsynedit.pas | 39 +- components/synedit/synedit.pp | 6 +- components/synedit/syneditmarkup.pp | 6 +- components/synedit/syneditmarkuphighall.pp | 1839 ++++++++++++++--- components/synedit/test/testbase.pas | 10 +- components/synedit/test/testmarkuphighall.pas | 694 +++++++ 7 files changed, 2276 insertions(+), 319 deletions(-) create mode 100644 components/synedit/test/testmarkuphighall.pas diff --git a/.gitattributes b/.gitattributes index 9c069427d9..68ab58b7ad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2883,6 +2883,7 @@ components/synedit/test/testhighlightfoldbase.pas svneol=native#text/pascal components/synedit/test/testhighlightmulti.pas svneol=native#text/pascal components/synedit/test/testhighlightpas.pas svneol=native#text/pascal components/synedit/test/testhighlightxml.pas svneol=native#text/pascal +components/synedit/test/testmarkuphighall.pas svneol=native#text/pascal components/synedit/test/testmarkupwordgroup.pas svneol=native#text/pascal components/synedit/test/testnavigation.pas svneol=native#text/pascal components/synedit/test/testsearch.pas svneol=native#text/pascal diff --git a/components/synedit/allsynedit.pas b/components/synedit/allsynedit.pas index 34a6c203a9..b4dc72965b 100644 --- a/components/synedit/allsynedit.pas +++ b/components/synedit/allsynedit.pas @@ -9,27 +9,24 @@ interface uses SynBeautifier, SynCompletion, SynEdit, SynEditAutoComplete, SynEditExport, SynEditFoldedView, SynEditHighlighter, SynEditHighlighterFoldBase, - SynEditHighlighterXMLBase, SynEditKeyCmds, LazSynEditMouseCmdsTypes, - SynHighlighterPo, SynEditLines, SynEditMarks, SynEditMarkup, - SynEditMarkupBracket, SynEditMarkupCtrlMouseLink, SynEditMarkupHighAll, - SynEditMarkupSelection, SynEditMarkupSpecialLine, SynEditMarkupWordGroup, - SynEditMiscClasses, SynEditMiscProcs, SynEditMouseCmds, SynEditPlugins, - SynEditPointClasses, SynEditRegexSearch, SynEditSearch, SynEditStrConst, - SynEditTextBase, SynEditTextBuffer, SynEditTextBidiChars, - SynEditTextTabExpander, SynEditTextTrimmer, SynEditTypes, SynExportHTML, - SynGutter, SynGutterBase, SynGutterChanges, SynGutterCodeFolding, - SynGutterLineNumber, SynGutterLineOverview, SynGutterMarks, - SynHighlighterAny, SynHighlighterCpp, SynHighlighterCss, SynHighlighterDiff, - SynHighlighterHashEntries, SynHighlighterHTML, SynHighlighterJava, - SynHighlighterJScript, SynHighlighterLFM, SynHighlighterMulti, - SynHighlighterPas, SynHighlighterPerl, SynHighlighterPHP, - SynHighlighterPosition, SynHighlighterPython, SynHighlighterSQL, - SynHighlighterTeX, synhighlighterunixshellscript, SynHighlighterVB, - SynHighlighterXML, SynMacroRecorder, SynMemo, SynPluginSyncroEdit, - SynPluginSyncronizedEditBase, SynPluginTemplateEdit, LazSynEditText, - LazSynTextArea, SynRegExpr, SynTextDrawer, SynEditMarkupGutterMark, - SynHighlighterBat, SynHighlighterIni, SynEditMarkupSpecialChar, - SynEditTextDoubleWidthChars, LazarusPackageIntf; + SynEditHighlighterXMLBase, SynEditKeyCmds, LazSynEditMouseCmdsTypes, SynHighlighterPo, + SynEditLines, SynEditMarks, SynEditMarkup, SynEditMarkupBracket, + SynEditMarkupCtrlMouseLink, SynEditMarkupHighAll, SynEditMarkupSelection, + SynEditMarkupSpecialLine, SynEditMarkupWordGroup, SynEditMiscClasses, SynEditMiscProcs, + SynEditMouseCmds, SynEditPlugins, SynEditPointClasses, SynEditRegexSearch, SynEditSearch, + SynEditStrConst, SynEditTextBase, SynEditTextBuffer, SynEditTextBidiChars, + SynEditTextTabExpander, SynEditTextTrimmer, SynEditTypes, SynExportHTML, SynGutter, + SynGutterBase, SynGutterChanges, SynGutterCodeFolding, SynGutterLineNumber, + SynGutterLineOverview, SynGutterMarks, SynHighlighterAny, SynHighlighterCpp, + SynHighlighterCss, SynHighlighterDiff, SynHighlighterHashEntries, SynHighlighterHTML, + SynHighlighterJava, SynHighlighterJScript, SynHighlighterLFM, SynHighlighterMulti, + SynHighlighterPas, SynHighlighterPerl, SynHighlighterPHP, SynHighlighterPosition, + SynHighlighterPython, SynHighlighterSQL, SynHighlighterTeX, synhighlighterunixshellscript, + SynHighlighterVB, SynHighlighterXML, SynMacroRecorder, SynMemo, SynPluginSyncroEdit, + SynPluginSyncronizedEditBase, SynPluginTemplateEdit, LazSynEditText, LazSynTextArea, + SynRegExpr, SynTextDrawer, SynEditMarkupGutterMark, SynHighlighterBat, SynHighlighterIni, + SynEditMarkupSpecialChar, SynEditTextDoubleWidthChars, SynEditTextSystemCharWidth, + LazarusPackageIntf; implementation diff --git a/components/synedit/synedit.pp b/components/synedit/synedit.pp index 7f9304021f..bb9a0a96d9 100644 --- a/components/synedit/synedit.pp +++ b/components/synedit/synedit.pp @@ -673,7 +673,6 @@ type procedure SetGutter(const Value: TSynGutter); procedure SetRightGutter(const AValue: TSynGutter); procedure SetHideSelection(const Value: boolean); - procedure SetHighlighter(const Value: TSynCustomHighlighter); procedure RemoveHooksFromHighlighter; procedure SetInsertCaret(const Value: TSynEditCaretType); procedure SetInsertMode(const Value: boolean); @@ -781,6 +780,7 @@ type AnInfo: TSynEditMouseActionInfo): Boolean; protected + procedure SetHighlighter(const Value: TSynCustomHighlighter); virtual; procedure SetVisible(Value: Boolean); override; procedure SetColor(Value: TColor); override; procedure DragOver(Source: TObject; X, Y: Integer; @@ -2209,9 +2209,9 @@ begin FInvalidateRect := Rect(-1, -1, -2, -2); FOldTopView := TopView; FLastTextChangeStamp := TSynEditStringList(FLines).TextChangeStamp; + FMarkupManager.IncPaintLock; end; inc(FPaintLock); - FMarkupManager.IncPaintLock; FFoldedLinesView.Lock; //DecPaintLock triggers ScanFrom, and folds must wait FTrimmedLinesView.Lock; // Lock before caret FBlockSelection.Lock; @@ -2246,7 +2246,6 @@ begin FBlockSelection.Unlock; FTrimmedLinesView.UnLock; // Must be unlocked after caret // May Change lines FFoldedLinesView.UnLock; // after ScanFrom, but before UpdateCaret - FMarkupManager.DecPaintLock; Dec(FPaintLock); if (FPaintLock = 0) and HandleAllocated then begin ScrollAfterTopLineChanged; @@ -2267,6 +2266,7 @@ begin // Todo: Markup can do invalidation, should be before ScrollAfterTopLineChanged; end; if (FPaintLock = 0) then begin + FMarkupManager.DecPaintLock; FBlockSelection.AutoExtend := False; if fStatusChanges <> [] then DoOnStatusChange(fStatusChanges); diff --git a/components/synedit/syneditmarkup.pp b/components/synedit/syneditmarkup.pp index 23c50dc04f..dc08d209dc 100644 --- a/components/synedit/syneditmarkup.pp +++ b/components/synedit/syneditmarkup.pp @@ -419,7 +419,8 @@ end; procedure TSynEditMarkup.DecPaintLock; begin - dec(FPaintLock); + if FPaintLock > 0 then + dec(FPaintLock); end; procedure TSynEditMarkup.PrepareMarkupForRow(aRow: Integer); @@ -463,8 +464,7 @@ begin TSynEditMarkup(fMarkUpList[i]).DecPaintLock; end; -procedure TSynEditMarkupManager.AddMarkUp(aMarkUp: TSynEditMarkup; - AsFirst: Boolean); +procedure TSynEditMarkupManager.AddMarkUp(aMarkUp: TSynEditMarkup; AsFirst: Boolean); begin if AsFirst then fMarkUpList.Insert(0, aMarkUp) diff --git a/components/synedit/syneditmarkuphighall.pp b/components/synedit/syneditmarkuphighall.pp index b4dd8a5e9c..6a3e2694da 100644 --- a/components/synedit/syneditmarkuphighall.pp +++ b/components/synedit/syneditmarkuphighall.pp @@ -28,7 +28,7 @@ interface uses Classes, SysUtils, ExtCtrls, SynEditMarkup, SynEditTypes, SynEditSearch, SynEditMiscClasses, Controls, LCLProc, SynEditHighlighter, SynEditPointClasses, - SynEditMiscProcs, SynEditFoldedView; + SynEditMiscProcs, SynEditFoldedView, LazClasses; type @@ -69,15 +69,11 @@ type end; - { TSynEditMarkupHighlightAll } + { TSynEditMarkupHighlightAllBase } - TSynEditMarkupHighlightAll = class(TSynEditMarkup) + TSynEditMarkupHighlightAllBase = class(TSynEditMarkup) private FFoldView: TSynEditFoldedView; - FSearchString : string; // for highlighting all occurences of a word/term - FSearchOptions: TSynSearchOptions; - FSearchStringMaxLines: Integer; - FSearch: TSynEditSearch; FNextPosIdx, FNextPosRow: Integer; FNeedValidate, FNeedValidatePaint: Boolean; FMarkupEnabled: Boolean; @@ -91,20 +87,20 @@ type function GetMatchCount: Integer; procedure SetFoldView(AValue: TSynEditFoldedView); procedure SetHideSingleMatch(AValue: Boolean); - procedure SetSearchOptions(const AValue : TSynSearchOptions); procedure DoFoldChanged(aLine: Integer); Procedure ValidateMatches(SkipPaint: Boolean = False); - function SearchStringMaxLines: Integer; - procedure FindInitialize; + protected + function HasSearchData: Boolean; virtual; abstract; + function SearchStringMaxLines: Integer; virtual; abstract; + procedure FindInitialize; virtual; abstract; function FindMatches(AStartPoint, AEndPoint: TPoint; var AIndex: Integer; AStopAfterLine: Integer = -1; // AEndPoint may be set further down, for multi-line matches ABackward : Boolean = False - ): TPoint; // returns searhed until point - protected - procedure SetSearchString(const AValue : String); virtual; + ): TPoint; virtual; abstract; // returns searhed until point + procedure DoTopLineChanged(OldTopLine : Integer); override; procedure DoLinesInWindoChanged(OldLinesInWindow : Integer); override; @@ -113,6 +109,7 @@ type procedure DoVisibleChanged(AVisible: Boolean); override; function HasVisibleMatch: Boolean; // does not check, if in visible line range. Only Count and DideSingleMatch property MatchCount: Integer read GetMatchCount; + property Matches: TSynMarkupHighAllMatchList read FMatches; property MarkupEnabled: Boolean read FMarkupEnabled; public constructor Create(ASynEdit : TSynEditBase); @@ -121,6 +118,7 @@ type procedure DecPaintLock; override; procedure PrepareMarkupForRow(aRow: Integer); override; + procedure EndMarkup; override; function GetMarkupAttributeAtRowCol(const aRow: Integer; const aStartCol: TLazSynDisplayTokenBound; const AnRtlInfo: TLazSynDisplayRtlInfo): TSynSelectedColor; override; @@ -136,11 +134,223 @@ type property FoldView: TSynEditFoldedView read FFoldView write SetFoldView; - property SearchString : String read fSearchString write SetSearchString; - property SearchOptions : TSynSearchOptions read fSearchOptions write SetSearchOptions; property HideSingleMatch: Boolean read FHideSingleMatch write SetHideSingleMatch; end; + { TSynEditMarkupHighlightAll } + + TSynEditMarkupHighlightAll = class(TSynEditMarkupHighlightAllBase) + private + FSearch: TSynEditSearch; + FSearchOptions: TSynSearchOptions; + FSearchString: String; + FSearchStringMaxLines: Integer; + + procedure SetSearchOptions(AValue: TSynSearchOptions); + procedure SetSearchString(AValue: String); + protected + procedure SearchStringChanged; virtual; + + function HasSearchData: Boolean; override; + function SearchStringMaxLines: Integer; override; + procedure FindInitialize; override; + function FindMatches(AStartPoint, AEndPoint: TPoint; + var AIndex: Integer; + AStopAfterLine: Integer = -1; // AEndPoint may be set further down, for multi-line matches + ABackward : Boolean = False + ): TPoint; override; // returns searhed until point + public + constructor Create(ASynEdit: TSynEditBase); + destructor Destroy; override; + property SearchString : String read FSearchString write SetSearchString; + property SearchOptions : TSynSearchOptions read FSearchOptions write SetSearchOptions; + end; + + { TSynSearchDictionary } + + PSynSearchDictionaryNode = ^TSynSearchDictionaryNode; + TSynSearchDictionaryNode = record + NextCharMin, NextCharMax: Byte; // if char > 128 then char := 128+256 - char // move utf8 continuation block + ItemIdx: Integer; // Node is in dictionary + NotFoundEntry: PSynSearchDictionaryNode; + NextEntry: Array [0..191] of PSynSearchDictionaryNode; // Max size 192, for utf8 start bytes + end; + + TSynSearchDictFoundEvent = + procedure(MatchEnd: PChar; MatchIdx: Integer; + var IsMatch: Boolean; var StopSeach: Boolean + ) of object; + + TSynSearchTermOptsBounds = (soNoBounds, soBothBounds, soBoundsAtStart, soBoundsAtEnd); + + { TSynSearchDictionary } + + TSynSearchDictionary = class(TObject) + private + FBuildLowerCaseDict: Boolean; + FList: TStringList; + FSortedList: TStringList; + FRootNode: PSynSearchDictionaryNode; + + procedure ClearDictionary; + procedure DeleteNode(aNode: PSynSearchDictionaryNode); + procedure BuildDictionary; + function GetTerms(AIndex: Integer): String; + procedure SetTerms(AIndex: Integer; AValue: String); + public + constructor Create; + destructor Destroy; override; + {$IFDEF SynDictDebug} + procedure DebugPrint(OnlySummary: Boolean = false); + {$ENDIF} + procedure Clear; + + function Add(ATerm: String; ATag: Integer): Integer; + function IndexOf(ATerm: String): Integer; + procedure Remove(ATerm: String); + procedure Delete(AIndex: Integer); + function Count: Integer; + property Terms[AIndex: Integer]: String read GetTerms write SetTerms; + + procedure Search(AText: PChar; ATextLen: Integer; AFoundEvent: TSynSearchDictFoundEvent); + + property BuildLowerCaseDict: Boolean read FBuildLowerCaseDict write FBuildLowerCaseDict; + end; + + TSynEditMarkupHighlightAllMulti = class; + + { TSynSearchTerm } + + TSynSearchTerm = class(TCollectionItem) + private + FEnabled: Boolean; + FMatchCase: Boolean; + FMatchWordBounds: TSynSearchTermOptsBounds; + FSearchTerm: String; + procedure SetEnabled(AValue: Boolean); + procedure SetMatchCase(AValue: Boolean); + procedure SetMatchWordBounds(AValue: TSynSearchTermOptsBounds); + procedure SetSearchTerm(AValue: String); + public + constructor Create(ACollection: TCollection); override; + procedure Assign(Source: TPersistent); override; + function Equals(Other: TSynSearchTerm): boolean; reintroduce; + published + property SearchTerm: String read FSearchTerm write SetSearchTerm; + property MatchWordBounds: TSynSearchTermOptsBounds read FMatchWordBounds write SetMatchWordBounds; + property MatchCase: Boolean read FMatchCase write SetMatchCase; + property Enabled: Boolean read FEnabled write SetEnabled; // Todo: Exclude from dict, but need to keep room for ID/Index + end; + + TSynSearchTermClass = class of TSynSearchTerm; + + { TSynSearchTermList } + + TSynSearchTermList = class(TCollection) + private + FOnChanged: TNotifyEvent; + function GetItem(Index: Integer): TSynSearchTerm; + procedure SetItem(Index: Integer; AValue: TSynSearchTerm); + protected + procedure Update(Item: TCollectionItem); override; + procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); override; + function DefaultItemClass: TSynSearchTermClass; virtual; + public + constructor Create; overload; + function Add: TSynSearchTerm; reintroduce; + function IndexOfSearchTerm(ATerm: String; ASearchStartIdx: Integer = 0): Integer; + function IndexOfSearchTerm(ATerm: TSynSearchTerm; ASearchStartIdx: Integer = 0): Integer; + function IndexOfSearchTerm(ATerm: String; ACaseSensitive: Boolean; ASearchStartIdx: Integer = 0): Integer; + property Items[Index: Integer]: TSynSearchTerm read GetItem write SetItem; default; + property OnChanged: TNotifyEvent read FOnChanged write FOnChanged; + end; + + TSynSearchTermListClass = class of TSynSearchTermList; + + { TSynSearchTermDict } + + TSynSearchTermDict = class(TRefCountedObject) + private + FTerms: TSynSearchTermList; + FDict: TSynSearchDictionary; + FNextTermWithSameWord: Array of Integer; + FChangedNotifyList: TMethodList; + FChangeNotifyLock: Integer; + FNeedNotify: Boolean; + + procedure DoTermsChanged(Sender: TObject); + function GetItem(Index: Integer): TSynSearchTerm; + procedure SetItem(Index: Integer; AValue: TSynSearchTerm); + protected + procedure MaybeInitDict; + property Dict: TSynSearchDictionary read FDict; + property Terms: TSynSearchTermList read FTerms; + public + constructor Create(ATermListClass: TSynSearchTermListClass); + destructor Destroy; override; + procedure IncChangeNotifyLock; + procedure DecChangeNotifyLock; + procedure RegisterChangedHandler(AEvent: TNotifyEvent); + procedure UnRegisterChangedHandler(AEvent: TNotifyEvent); + Procedure Assign(Src: TSynSearchTermDict); + Procedure Assign(Src: TSynSearchTermList); + + procedure Clear; + procedure ClearDictionary; + function Count: Integer; + function Add: TSynSearchTerm; + procedure Delete(AIndex: Integer); + function IndexOfSearchTerm(ATerm: String): Integer; + function IndexOfSearchTerm(ATerm: TSynSearchTerm): Integer; + + procedure Search(AText: PChar; ATextLen: Integer; AFoundEvent: TSynSearchDictFoundEvent); + function GetIndexForNextWordOccurrence(AIndex: Integer): Integer; + + property Items[Index: Integer]: TSynSearchTerm read GetItem write SetItem; default; + end; + + { TSynEditMarkupHighlightAllMulti } + + TSynEditMarkupHighlightAllMulti = class(TSynEditMarkupHighlightAllBase) + private + FTermDict: TSynSearchTermDict; + //FNextTermWIthSameWord: Array of Integer; + + FFindInsertIndex: Integer; + FFindLineY: Integer; + FFindLineText, FFindLineTextEnd, FFindLineTextLower, FFindLineTextLowerEnd: PChar; + FBackward, FBackwardReplace: Boolean; + FWordBreakChars: TSynIdentChars; + + procedure DoMatchFound(MatchEnd: PChar; MatchIdx: Integer; var IsMatch: Boolean; + var StopSeach: Boolean); + procedure SetWordBreakChars(AValue: TSynIdentChars); + protected + procedure DoTermsChanged(Sender: TObject); + function HasSearchData: Boolean; override; + function SearchStringMaxLines: Integer; override; + procedure FindInitialize; override; + function FindMatches(AStartPoint, AEndPoint: TPoint; + var AIndex: Integer; + AStopAfterLine: Integer = -1; // AEndPoint may be set further down, for multi-line matches + ABackward : Boolean = False + ): TPoint; override; // returns searhed until point + function CreateTermsList: TSynSearchTermDict; virtual; + public + constructor Create(ASynEdit: TSynEditBase); + destructor Destroy; override; + procedure Clear; + procedure ResetWordBreaks; + + function AddSearchTerm(ATerm: String): Integer; + function IndexOfSearchTerm(ATerm: String): Integer; + procedure RemoveSearchTerm(ATerm: String); + procedure DeleteSearchTerm(AIndex: Integer); + + property WordBreakChars: TSynIdentChars read FWordBreakChars write SetWordBreakChars; + property Terms: TSynSearchTermDict read FTermDict; + end; + { TSynEditMarkupHighlightAllCaret } TSynEditMarkupHighlightAllCaret = class(TSynEditMarkupHighlightAll) @@ -166,7 +376,7 @@ type procedure SetTrim(const AValue: Boolean); procedure SetWaitTime(const AValue: Integer); protected - procedure SetSearchString(const AValue : String); override; + procedure SearchStringChanged; override; procedure SelectionChanged(Sender: TObject); procedure DoCaretChanged(Sender: TObject); override; procedure DoTextChanged(StartLine, EndLine, ACountDiff: Integer); override; @@ -200,51 +410,1002 @@ const MATCHES_CLEAN_LINE_THRESHOLD = 300; // Remove matches out of range, only if they are at least n lines from visible area. MATCHES_CLEAN_LINE_KEEP = 200; // LinesKept, if cleaning. MUST be LESS than MATCHES_CLEAN_LINE_THRESHOLD -{ TSynMarkupHighAllLines } +{ TSynSearchTermDict } -(* -constructor TSynMarkupHighAllLines.Create; +procedure TSynSearchTermDict.DoTermsChanged(Sender: TObject); begin - FCount := 0; - FCapacity := 128; - SetLength(FLines, FCapacity); + if FDict = nil then + exit; + + FDict.Clear; + if FChangeNotifyLock > 0 then begin + FNeedNotify := True; + exit; + end; + FNeedNotify := False; + FChangedNotifyList.CallNotifyEvents(Self); end; -function TSynMarkupHighAllLines.MaybeReduceCapacity : Boolean; +function TSynSearchTermDict.GetItem(Index: Integer): TSynSearchTerm; begin - if not( (FCapacity > 128) and (FCapacity > FCount*4) ) - then exit(False); - - FCapacity := FCapacity div 2; - SetLength(FLines, FCapacity); - result := true; + Result := FTerms[Index]; end; -procedure TSynMarkupHighAllLines.SetCount(const AValue : Integer); - +procedure TSynSearchTermDict.IncChangeNotifyLock; begin - if FCount=AValue then exit; - FCount:=AValue; - - if MaybeReduceCapacity - then exit; - if (FCapacity <= FCount) - then FCapacity := FCapacity * 2 - else exit; - SetLength(FLines, FCapacity); + inc(FChangeNotifyLock); end; -function TSynMarkupHighAllLines.GetLine(const Index : Integer) : TSynMarkupHighAllLine; +procedure TSynSearchTermDict.DecChangeNotifyLock; begin - Result := FLines[Index]; + dec(FChangeNotifyLock); + if FNeedNotify then + DoTermsChanged(Self); end; -procedure TSynMarkupHighAllLines.SetLine(const Index : Integer; const AValue : TSynMarkupHighAllLine); +procedure TSynSearchTermDict.SetItem(Index: Integer; AValue: TSynSearchTerm); begin -// if Index = Count then Count := Count + 1; // AutoIncrease - FLines[Index] := AValue; + FTerms[Index] := AValue; +end; + +procedure TSynSearchTermDict.MaybeInitDict; +var + i, j: Integer; + s: String; +begin + if FDict.Count > 0 then + exit; + + SetLength(FNextTermWIthSameWord, FTerms.Count); + + for i := 0 to FTerms.Count - 1 do begin + FNextTermWIthSameWord[i] := -1; + if not FTerms[i].Enabled then + Continue; + s := LowerCase(FTerms[i].SearchTerm); + FDict.Add(FTerms[i].SearchTerm, i); + for j := i + 1 to FTerms.Count - 1 do + if LowerCase(FTerms[j].SearchTerm) = s then begin + FNextTermWIthSameWord[i] := j; + break; + end; + end; +end; + +constructor TSynSearchTermDict.Create(ATermListClass: TSynSearchTermListClass); +begin + inherited Create; + FChangedNotifyList := TMethodList.Create; + FNeedNotify := False; + FTerms := ATermListClass.Create; + FTerms.OnChanged := @DoTermsChanged; + FDict := TSynSearchDictionary.Create; + FDict.BuildLowerCaseDict := True; +end; + +destructor TSynSearchTermDict.Destroy; +begin + inherited Destroy; + FChangeNotifyLock := 1; // Disable notifications + FreeAndNil(FDict); + FreeAndNil(FTerms); + FreeAndNil(FChangedNotifyList); +end; + +procedure TSynSearchTermDict.RegisterChangedHandler(AEvent: TNotifyEvent); +begin + FChangedNotifyList.Add(TMethod(AEvent)); +end; + +procedure TSynSearchTermDict.UnRegisterChangedHandler(AEvent: TNotifyEvent); +begin + FChangedNotifyList.Remove(TMethod(AEvent)); +end; + +procedure TSynSearchTermDict.Assign(Src: TSynSearchTermDict); +begin + IncChangeNotifyLock; + FDict.Clear; + FTerms.Assign(Src.FTerms); + DecChangeNotifyLock; +end; + +procedure TSynSearchTermDict.Assign(Src: TSynSearchTermList); +begin + IncChangeNotifyLock; + FDict.Clear; + FTerms.Assign(Src); + DecChangeNotifyLock; +end; + +procedure TSynSearchTermDict.Clear; +begin + IncChangeNotifyLock; + FTerms.Clear; + FDict.Clear; + DecChangeNotifyLock; +end; + +procedure TSynSearchTermDict.ClearDictionary; +begin + FDict.Clear; +end; + +function TSynSearchTermDict.Count: Integer; +begin + Result := FTerms.Count; +end; + +function TSynSearchTermDict.Add: TSynSearchTerm; +begin + Result := TSynSearchTerm(FTerms.Add); +end; + +procedure TSynSearchTermDict.Delete(AIndex: Integer); +begin + FTerms.Delete(AIndex); +end; + +function TSynSearchTermDict.IndexOfSearchTerm(ATerm: String): Integer; +begin + Result := FTerms.IndexOfSearchTerm(ATerm); +end; + +function TSynSearchTermDict.IndexOfSearchTerm(ATerm: TSynSearchTerm): Integer; +begin + Result := FTerms.IndexOfSearchTerm(ATerm); +end; + +procedure TSynSearchTermDict.Search(AText: PChar; ATextLen: Integer; + AFoundEvent: TSynSearchDictFoundEvent); +begin + MaybeInitDict; + FDict.Search(AText, ATextLen, AFoundEvent); +end; + +function TSynSearchTermDict.GetIndexForNextWordOccurrence(AIndex: Integer): Integer; +begin + Result := FNextTermWIthSameWord[AIndex]; +end; + +{ TSynSearchTermList } + + +{ TSynSearchDictionary } + +procedure TSynSearchDictionary.ClearDictionary; +begin + DeleteNode(FRootNode); + FRootNode := nil; +end; + +procedure TSynSearchDictionary.DeleteNode(aNode: PSynSearchDictionaryNode); +var + i: Integer; +begin + if aNode = nil then + exit; + For i := 0 to aNode^.NextCharMax - aNode^.NextCharMin do + DeleteNode(aNode^.NextEntry[i]); + FreeMem(aNode); +end; + +function CompareBinary(List: TStringList; Index1, Index2: Integer): Integer; +var + s1, s2: String; + l: Integer; +begin + Result := 0; + s1 := List[Index1]; + s2 := List[Index2]; + l := Length(s1); + if Length(s2) < l then begin + l := Length(s2); + if l > 0 then + Result := CompareByte(s1[1], s2[1], l); + if Result = 0 then + Result := 1; + end else begin + if l > 0 then + Result := CompareByte(s1[1], s2[1], l); + if Result = 0 then + Result := -1; + end; +end; + +procedure TSynSearchDictionary.BuildDictionary; + + function ChangeBytes(ATerm: String): String; + var + i: Integer; + c: Char; + begin + (* Map utf8 continuation bytes (128..191) to the end of the char-range (192..255) + This way the max size for "NextEntry" array will be 192 (for utf8 char start) + or 64 for continuation + Also by mapping #252..#255 to #188..#191, makes them a requiremnt for any + node having a full 192 sized array. This will reduce the risk of worst case + memory consumption, since they have 4 continuation bytes (array size 64) + to bring down the average. + *) + SetLength(Result, Length(ATerm)); + for i := 1 to Length(ATerm) do begin + c := ATerm[i]; + if c < #128 + then Result[i] := c + else Result[i] := chr(383-ord(c)); + end; + end; + + function GetNodeForCharAt(AListIndex, AMaxListIdx, ACharPos: Integer) :PSynSearchDictionaryNode; + var + c: Char; + i, LastListIdx, MatchIdx, MinChar, MaxChar: Integer; + begin + // Find all continuation chars + if ACharPos = 0 then begin + LastListIdx := AMaxListIdx; + end + else begin + c := FSortedList[AListIndex][ACharPos]; + LastListIdx := AListIndex; + while (LastListIdx < AMaxListIdx) and + (length(FSortedList[LastListIdx+1]) >= ACharPos) and + (FSortedList[LastListIdx+1][ACharPos] = c) + do + inc(LastListIdx); + end; + + if length(FSortedList[AListIndex]) = ACharPos then + MatchIdx := PtrInt(FSortedList.Objects[AListIndex]) // this is a match, TODO: there could be sevelal matches of the same length + else + MatchIdx := -1; + while (AListIndex <= LastListIdx) and (length(FSortedList[AListIndex]) = ACharPos) do begin + // for identical words, store smallest matchidx (TODO: true case sensitive search) + if PtrInt(FSortedList.Objects[AListIndex]) < MatchIdx then + MatchIdx := PtrInt(FSortedList.Objects[AListIndex]); + inc(AListIndex); // Skip match, if any + end; + + if length(FSortedList[LastListIdx]) > ACharPos then begin + // there are possible continuations + MinChar := ord(FSortedList[AListIndex][ACharPos+1]); + MaxChar := ord(FSortedList[LastListIdx][ACharPos+1]); + end + else begin + // No continuatian + MinChar := 1; + MaxChar := 0; + end; + + Result := AllocMem(PtrUInt(@PSynSearchDictionaryNode(nil)^.NextEntry[0]) + + PtrUInt(MaxChar - MinChar + 1)*SizeOf(PSynSearchDictionaryNode)); + Result^.NextCharMin := MinChar; + Result^.NextCharMax := MaxChar; + Result^.ItemIdx := MatchIdx; + + inc(ACharPos); + for i := MinChar to MaxChar do begin + c := FSortedList[AListIndex][ACharPos]; + if c = chr(i) then begin + Result^.NextEntry[i-MinChar] := GetNodeForCharAt(AListIndex, LastListIdx, ACharPos); + while (AListIndex < LastListIdx) and (FSortedList[AListIndex][ACharPos] = c) do + inc(AListIndex); + end + else + Result^.NextEntry[i-MinChar] := nil; + end; + end; + + function FindNode(ANodeValue: String) :PSynSearchDictionaryNode; + var + i, b, m: Integer; + begin + Result := FRootNode; + for i := 1 to length(ANodeValue) do begin + b := ord(ANodeValue[i]); + m := Result^.NextCharMin; + if (b < m) or (b > Result^.NextCharMax) or + (Result^.NextEntry[b-m] = nil) + then + exit(nil); + Result := Result^.NextEntry[b-m]; + end; + end; + + procedure SetNotFoundNote(ANode: PSynSearchDictionaryNode; ANodeValue: String); + var + i, m: Integer; + begin + if ANodeValue <> '' then begin + for i := 2 to length(ANodeValue) do begin + ANode^.NotFoundEntry := FindNode(copy(ANodeValue, i, length(ANodeValue))); + if ANode^.NotFoundEntry <> nil then + break; + end; + if ANode^.NotFoundEntry = nil then + ANode^.NotFoundEntry := FRootNode; + end; + + m := ANode^.NextCharMin; + for i := ANode^.NextCharMin to ANode^.NextCharMax do + if ANode^.NextEntry[i-m] <> nil then + SetNotFoundNote(ANode^.NextEntry[i-m], ANodeValue + chr(i)); + end; + + +var + i: Integer; +begin + ClearDictionary; + if FList.Count = 0 then + exit; + + FSortedList.Clear; + for i := 0 to FList.Count - 1 do begin + if FBuildLowerCaseDict then // TODO: Create a case-insesitive dictionary + FSortedList.AddObject(ChangeBytes(LowerCase(FList[i])), FList.Objects[i]) + else + FSortedList.AddObject(ChangeBytes(FList[i]), FList.Objects[i]); + end; + FSortedList.CustomSort(@CompareBinary); + + FRootNode := GetNodeForCharAt(0, FSortedList.Count - 1, 0); + SetNotFoundNote(FRootNode, ''); + FRootNode^.NotFoundEntry := nil; + + FSortedList.Clear; +end; + +function TSynSearchDictionary.GetTerms(AIndex: Integer): String; +begin + Result := FList[AIndex]; +end; + +procedure TSynSearchDictionary.SetTerms(AIndex: Integer; AValue: String); +begin + FList[AIndex] := AValue; + ClearDictionary; +end; + +constructor TSynSearchDictionary.Create; +begin + inherited Create; + FList := TStringList.Create; + FList.OwnsObjects := False; + FSortedList := TStringList.Create; + FSortedList.OwnsObjects := False; + FBuildLowerCaseDict := False; +end; + +destructor TSynSearchDictionary.Destroy; +begin + inherited Destroy; + Clear; + FreeAndNil(FList); + FreeAndNil(FSortedList); +end; + +{$IFDEF SynDictDebug} +procedure TSynSearchDictionary.DebugPrint(OnlySummary: Boolean); +var + NCnt, ArrayLen, EmptyCnt: Integer; + + function FlipByte(b: Integer): Integer; + begin + if b < 128 + then Result := b + else Result := 383-b; + end; + + procedure DebugNode(ANode: PSynSearchDictionaryNode; APreFix: String = ''; AIndent: String = ''); + var + i, j: Integer; + begin + inc(NCnt); + if not OnlySummary then + DebugLn([AIndent, 'Node for "', APreFix, '": ItemIdx=', ANode^.ItemIdx, + ' Min=', FlipByte(ANode^.NextCharMin), ' Max=', FlipByte(ANode^.NextCharMax), + ' At ', IntToHex(PtrUInt(ANode), 2*sizeof(PtrUInt)), + ' Not Found ', IntToHex(PtrUInt(ANode^.NotFoundEntry), 2*sizeof(PtrUInt)) + ]); + j := ANode^.NextCharMin; + ArrayLen := ArrayLen + ANode^.NextCharMax - ANode^.NextCharMin + 1; + for i := ANode^.NextCharMin to ANode^.NextCharMax do + if ANode^.NextEntry[i-j] <> nil then begin + if not OnlySummary then + debugln([AIndent, '> ', FlipByte(i)]); + DebugNode(ANode^.NextEntry[i-j], APreFix+chr(FlipByte(i)), AIndent+' '); + end + else + inc(EmptyCnt); + end; +begin + if FRootNode = nil then + BuildDictionary; + ArrayLen := 0; + NCnt := 0; + EmptyCnt := 0; + DebugNode(FRootNode); + DebugLn(['Nodes: ', NCnt, ' Sum(len(array))=', ArrayLen, ' Empty=', EmptyCnt]); +end; +{$ENDIF} + +procedure TSynSearchDictionary.Clear; +begin + FList.Clear; + ClearDictionary; +end; + +function TSynSearchDictionary.Add(ATerm: String; ATag: Integer): Integer; +begin + Result := FList.AddObject(ATerm, TObject(PtrInt(ATag))); + ClearDictionary; +end; + +function TSynSearchDictionary.IndexOf(ATerm: String): Integer; +begin + Result := FList.IndexOf(ATerm); +end; + +procedure TSynSearchDictionary.Remove(ATerm: String); +begin + FList.Delete(FList.IndexOf(ATerm)); + ClearDictionary; +end; + +procedure TSynSearchDictionary.Delete(AIndex: Integer); +begin + FList.Delete(AIndex); + ClearDictionary; +end; + +function TSynSearchDictionary.Count: Integer; +begin + Result := FList.Count; +end; + +procedure TSynSearchDictionary.Search(AText: PChar; ATextLen: Integer; + AFoundEvent: TSynSearchDictFoundEvent); +var + CurrentNode: PSynSearchDictionaryNode; + b, m: Integer; + IsMatch, DoWork: Boolean; + TextEnd: PChar; +begin + if FList.Count = 0 then + exit; + if FRootNode = nil then + BuildDictionary; + + DoWork := True; + CurrentNode := FRootNode; + TextEnd := AText + ATextLen; + b := ord(AText^); + if b > 128 then b := 383 - b; + + while true do begin + // CurrentNode is for (AText-1)^ + // b is for AText^ + if CurrentNode^.ItemIdx >= 0 then begin + IsMatch := True; + AFoundEvent(AText, CurrentNode^.ItemIdx, IsMatch, DoWork); + if not DoWork then + exit; + if IsMatch then + CurrentNode := FRootNode; // Do not do overlapping matches + end; + + m := CurrentNode^.NextCharMin; + if (b >= m) and (b <= CurrentNode^.NextCharMax) and + (CurrentNode^.NextEntry[b-m] <> nil) + then begin + CurrentNode := CurrentNode^.NextEntry[b-m]; + inc(AText); + if AText > TextEnd then + exit; + b := ord(AText^); + if b > 128 then b := 383 - b; + continue; + end; + + CurrentNode := CurrentNode^.NotFoundEntry; + if CurrentNode = nil then begin // last node was FRootNode; + CurrentNode := FRootNode; + inc(AText); + if AText > TextEnd then + break; + b := ord(AText^); + if b > 128 then b := 383 - b; + end; + end; +end; + +{ TSynSearchTerm } + +procedure TSynSearchTerm.SetMatchCase(AValue: Boolean); +begin + if FMatchCase = AValue then Exit; + FMatchCase := AValue; + Changed(False); +end; + +procedure TSynSearchTerm.SetEnabled(AValue: Boolean); +begin + if FEnabled = AValue then Exit; + FEnabled := AValue; + Changed(False); +end; + +procedure TSynSearchTerm.SetMatchWordBounds(AValue: TSynSearchTermOptsBounds); +begin + if FMatchWordBounds = AValue then Exit; + FMatchWordBounds := AValue; + Changed(False); +end; + +procedure TSynSearchTerm.SetSearchTerm(AValue: String); +begin + if FSearchTerm = AValue then Exit; + FSearchTerm := AValue; + Changed(False); +end; + +constructor TSynSearchTerm.Create(ACollection: TCollection); +begin + inherited Create(ACollection); + FMatchCase := False; + FMatchWordBounds := soNoBounds; + FEnabled := True; +end; + +procedure TSynSearchTerm.Assign(Source: TPersistent); +begin + if not(Source is TSynSearchTerm) then + exit; + FSearchTerm := TSynSearchTerm(Source).FSearchTerm; + FMatchCase := TSynSearchTerm(Source).FMatchCase; + FMatchWordBounds := TSynSearchTerm(Source).FMatchWordBounds; + FEnabled := TSynSearchTerm(Source).FEnabled; + Changed(False); +end; + +function TSynSearchTerm.Equals(Other: TSynSearchTerm): boolean; +begin + Result := (FMatchCase = Other.FMatchCase) and + (FMatchWordBounds = Other.FMatchWordBounds) and + (FSearchTerm = Other.FSearchTerm); +end; + +{ TSynSearchTermList } + +function TSynSearchTermList.GetItem(Index: Integer): TSynSearchTerm; +begin + Result := TSynSearchTerm(inherited GetItem(Index)); +end; + +procedure TSynSearchTermList.SetItem(Index: Integer; AValue: TSynSearchTerm); +begin + inherited SetItem(Index, AValue); +end; + +procedure TSynSearchTermList.Update(Item: TCollectionItem); +begin + inherited Update(Item); + if assigned(FOnChanged) then + FOnChanged(Self); +end; + +procedure TSynSearchTermList.Notify(Item: TCollectionItem; Action: TCollectionNotification); +begin + inherited Notify(Item, Action); + if assigned(FOnChanged) then + FOnChanged(Self); +end; + +function TSynSearchTermList.DefaultItemClass: TSynSearchTermClass; +begin + Result := TSynSearchTerm; +end; + +constructor TSynSearchTermList.Create; +begin + inherited Create(DefaultItemClass); +end; + +function TSynSearchTermList.Add: TSynSearchTerm; +begin + Result := TSynSearchTerm(inherited Add); +end; + +function TSynSearchTermList.IndexOfSearchTerm(ATerm: String; + ASearchStartIdx: Integer): Integer; +begin + Result := IndexOfSearchTerm(ATerm, True, ASearchStartIdx); +end; + +function TSynSearchTermList.IndexOfSearchTerm(ATerm: TSynSearchTerm; + ASearchStartIdx: Integer): Integer; +var + c: Integer; +begin + Result := ASearchStartIdx; + c := Count ; + while (Result < c) and (not Items[Result].Equals(ATerm)) do + inc(Result); + if Result >= c then + Result := -1; +end; + +function TSynSearchTermList.IndexOfSearchTerm(ATerm: String; ACaseSensitive: Boolean; + ASearchStartIdx: Integer): Integer; +var + c: Integer; +begin + Result := ASearchStartIdx; + c := Count ; + if ACaseSensitive then begin + while (Result < c) and (Items[Result].SearchTerm <> ATerm) do + inc(Result); + end + else begin + ATerm := LowerCase(ATerm); + while (Result < c) and (LowerCase(Items[Result].SearchTerm) <> ATerm) do + inc(Result); + end; + if Result >= c then + Result := -1; +end; + +{ TSynEditMarkupHighlightAllMulti } + +function TSynEditMarkupHighlightAllMulti.HasSearchData: Boolean; +begin + Result := FTermDict.Count > 0; +end; + +function TSynEditMarkupHighlightAllMulti.SearchStringMaxLines: Integer; +begin + Result := 1; // Todo: implement multiline +end; + +procedure TSynEditMarkupHighlightAllMulti.FindInitialize; +begin + // +end; + +procedure TSynEditMarkupHighlightAllMulti.DoMatchFound(MatchEnd: PChar; MatchIdx: Integer; + var IsMatch: Boolean; var StopSeach: Boolean); +var + Len: Integer; + o: TSynSearchTerm; + MatchBegin: PChar; +begin + while MatchIdx >= 0 do begin + o := FTermDict[MatchIdx]; + + if not o.Enabled then begin + MatchIdx := FTermDict.GetIndexForNextWordOccurrence(MatchIdx); + continue; + end; + + Len := length(o.SearchTerm); + MatchBegin := MatchEnd - Len - FFindLineTextLower + FFindLineText; + + if o.MatchCase and not(copy(MatchBegin, 1, Len) = o.SearchTerm) then begin + MatchIdx := FTermDict.GetIndexForNextWordOccurrence(MatchIdx); + continue; + end; + + if (o.MatchWordBounds in [soBoundsAtStart, soBothBounds]) and + not( (MatchBegin = FFindLineText) or + ((MatchBegin-1)^ in WordBreakChars) + ) + then begin + MatchIdx := FTermDict.GetIndexForNextWordOccurrence(MatchIdx); + continue; + end; + + if (o.MatchWordBounds in [soBoundsAtEnd, soBothBounds]) and + not( (MatchBegin+Len = FFindLineTextEnd) or + ((MatchBegin+Len)^ in WordBreakChars) + ) + then begin + MatchIdx := FTermDict.GetIndexForNextWordOccurrence(MatchIdx); + continue; + end; + + break; + end; + + IsMatch := MatchIdx >= 0; + if not IsMatch then + exit; + + if FBackwardReplace then begin + // Replace, only keep last match + FMatches.StartPoint[FFindInsertIndex] := Point(MatchBegin-FFindLineText+1, FFindLineY); + FMatches.EndPoint[FFindInsertIndex] := Point(MatchBegin-FFindLineText+1+Len, FFindLineY); + end + else + FMatches.Insert(FFindInsertIndex, + Point(MatchBegin-FFindLineText+1, FFindLineY), + Point(MatchBegin-FFindLineText+1+Len, FFindLineY) + ); + if not FBackward then + inc(FFindInsertIndex) + else + FBackwardReplace := True; +end; + +procedure TSynEditMarkupHighlightAllMulti.SetWordBreakChars(AValue: TSynIdentChars); +begin + if FWordBreakChars = AValue then Exit; + FWordBreakChars := AValue; + Invalidate; +end; + +procedure TSynEditMarkupHighlightAllMulti.DoTermsChanged(Sender: TObject); +begin + if (FTermDict = nil) then + exit; + Invalidate; +end; + +function TSynEditMarkupHighlightAllMulti.FindMatches(AStartPoint, AEndPoint: TPoint; + var AIndex: Integer; AStopAfterLine: Integer; ABackward: Boolean): TPoint; +var + LineLen: Integer; + LineText, LineTextLower: String; + x: integer; +begin + //debugln(['FindMatches IDX=', AIndex, ' Cnt=', Matches.Count, ' LCnt=', AEndPoint.y-AStartPoint.y , ' # ', FTerms[0].SearchTerm, ' # ',dbgs(AStartPoint),' - ',dbgs(AEndPoint), ' AStopAfterLine=',AStopAfterLine, ' Back=', dbgs(ABackward), ' ']); + FFindInsertIndex := AIndex; + FBackward := ABackward; // Currently supports only finding a single match + + if ABackward then begin + FBackwardReplace := False; + x := 1; + while AStartPoint.y <= AEndPoint.y do begin + LineText := Lines[AEndPoint.y-1]; + LineTextLower := LowerCase(LineText); + + LineLen := Min(Length(LineTextLower), AEndPoint.x-1); + if (AStartPoint.y = AEndPoint.y) and (AStartPoint.x > 1) then begin + x := AStartPoint.x - 1; + LineLen := Max(0, LineLen - x); + end; + + if LineLen > 0 then begin + FFindLineY := AEndPoint.Y; + FFindLineText := @LineText[1]; + FFindLineTextEnd := FFindLineText + LineLen; + FFindLineTextLower := @LineTextLower[1]; + FFindLineTextLowerEnd := FFindLineTextLower + LineLen; + if LineLen > 0 then + FTermDict.Search(@LineTextLower[x], LineLen, @DoMatchFound); + end; + + if FBackwardReplace then + break; // Only one supported + + dec(AEndPoint.y); + AEndPoint.x := MaxInt; + + //if (AStopAfterLine >= 0) and (AStartPoint.Y-1 > AStopAfterLine) and + // (FFindInsertIndex > AIndex) + //then begin + // AEndPoint := point(LineLen, AStartPoint.Y-1); + // break; + //end; + end; + + if FBackwardReplace then + inc(FFindInsertIndex); + end + else begin + while AStartPoint.y <= AEndPoint.y do begin + LineText := Lines[AStartPoint.y-1]; + LineTextLower := LowerCase(LineText); + + LineLen := Length(LineTextLower); + if AStartPoint.y = AEndPoint.y then + LineLen := Min(LineLen, AEndPoint.x - AStartPoint.x + 1); + + if LineLen > 0 then begin + FFindLineY := AStartPoint.Y; + FFindLineText := @LineText[1]; + FFindLineTextEnd := FFindLineText + LineLen; + FFindLineTextLower := @LineTextLower[1]; + FFindLineTextLowerEnd := FFindLineTextLower + LineLen; + if LineLen > 0 then + FTermDict.Search(@LineTextLower[1] + AStartPoint.x - 1, LineLen, @DoMatchFound); + end; + + inc(AStartPoint.y); + AStartPoint.x := 1; + + if (AStopAfterLine >= 0) and (AStartPoint.Y-1 > AStopAfterLine) and + (FFindInsertIndex > AIndex) + then begin + AEndPoint := point(LineLen, AStartPoint.Y-1); + break; + end; + end; + end; + + AIndex := FFindInsertIndex; + Result := AEndPoint; +end; + +function TSynEditMarkupHighlightAllMulti.CreateTermsList: TSynSearchTermDict; +begin + Result := TSynSearchTermDict.Create(TSynSearchTermList); +end; + +constructor TSynEditMarkupHighlightAllMulti.Create(ASynEdit: TSynEditBase); +begin + inherited Create(ASynEdit); + FTermDict := CreateTermsList; + FTermDict.AddReference; + FTermDict.RegisterChangedHandler(@DoTermsChanged); + ResetWordBreaks +end; + +destructor TSynEditMarkupHighlightAllMulti.Destroy; +begin + inherited Destroy; + ReleaseRefAndNil(FTermDict); +end; + +procedure TSynEditMarkupHighlightAllMulti.Clear; +begin + FTermDict.Clear; +end; + +procedure TSynEditMarkupHighlightAllMulti.ResetWordBreaks; +begin + FWordBreakChars := TSynWordBreakChars + TSynWhiteChars; + FTermDict.ClearDictionary; + Invalidate; +end; + +function TSynEditMarkupHighlightAllMulti.AddSearchTerm(ATerm: String): Integer; +var + Itm: TSynSearchTerm; +begin + Itm := FTermDict.Add; + Itm.SearchTerm := ATerm; + Result := Itm.Index; +end; + +function TSynEditMarkupHighlightAllMulti.IndexOfSearchTerm(ATerm: String): Integer; +begin + Result:= FTermDict.IndexOfSearchTerm(ATerm); +end; + +procedure TSynEditMarkupHighlightAllMulti.RemoveSearchTerm(ATerm: String); +begin + FTermDict.Delete(IndexOfSearchTerm(ATerm)); +end; + +procedure TSynEditMarkupHighlightAllMulti.DeleteSearchTerm(AIndex: Integer); +begin + FTermDict.Delete(AIndex); +end; + +{ TSynEditMarkupHighlightAll } + +procedure TSynEditMarkupHighlightAll.SetSearchOptions(AValue: TSynSearchOptions); +begin + if fSearchOptions = AValue then exit; + fSearchOptions := AValue; + FSearchStringMaxLines := -1; + Invalidate; +end; + +procedure TSynEditMarkupHighlightAll.SetSearchString(AValue: String); +begin + if FSearchString = AValue then exit; + FSearchString := AValue; + FSearchStringMaxLines := -1; + Invalidate; // bad if options and string search at the same time *and* string is <> '' + + SearchStringChanged; +end; + +procedure TSynEditMarkupHighlightAll.SearchStringChanged; +begin + // +end; + +function TSynEditMarkupHighlightAll.HasSearchData: Boolean; +begin + Result := FSearchString <> ''; +end; + +function TSynEditMarkupHighlightAll.SearchStringMaxLines: Integer; +var + i, j: Integer; +begin + Result := FSearchStringMaxLines; + if Result > 0 then + exit; + + if (fSearchOptions * [ssoRegExpr, ssoRegExprMultiLine] = []) + then begin + // can not wrap around lines + j := 1; + i := Length(fSearchString); + while i > 0 do begin + if fSearchString[i] = #13 then begin + inc(j); + if (i > 1) and (fSearchString[i-1] = #10) then dec(i); // skip alternating + end + else + if fSearchString[i] = #10 then begin + inc(j); + if (i > 1) and (fSearchString[i-1] = #13) then dec(i); // skip alternating + end; + dec(i); + end; + FSearchStringMaxLines := j; + end + else begin + if (fSearchOptions * [ssoRegExpr, ssoRegExprMultiLine] = [ssoRegExpr]) then + FSearchStringMaxLines := 1 // Only ssoRegExprMultiLine can expand accross lines (actually \n\r should anymay...) + else + FSearchStringMaxLines := 0; // Unknown + end; + + Result := FSearchStringMaxLines; +end; + +procedure TSynEditMarkupHighlightAll.FindInitialize; +begin + fSearch.Pattern := fSearchString; + fSearch.Sensitive := ssoMatchCase in fSearchOptions; + fSearch.Whole := ssoWholeWord in fSearchOptions; + fSearch.RegularExpressions := ssoRegExpr in fSearchOptions; + fSearch.RegExprMultiLine := ssoRegExprMultiLine in fSearchOptions; + fSearch.Backwards := False; +end; + +function TSynEditMarkupHighlightAll.FindMatches(AStartPoint, AEndPoint: TPoint; + var AIndex: Integer; AStopAfterLine: Integer; ABackward: Boolean): TPoint; +var + ptFoundStart, ptFoundEnd: TPoint; +begin + fSearch.Backwards := ABackward; + While (true) do begin + if not fSearch.FindNextOne(Lines, AStartPoint, AEndPoint, ptFoundStart, ptFoundEnd) + then break; + AStartPoint := ptFoundEnd; + + FMatches.Insert(AIndex, ptFoundStart, ptFoundEnd); + inc(AIndex); // BAckward learch needs final index to point to last inserted (currently support only find ONE) + + if (AStopAfterLine >= 0) and (ptFoundStart.Y > AStopAfterLine) then begin + AEndPoint := ptFoundEnd; + break; + end; + end; + Result := AEndPoint; +end; + +constructor TSynEditMarkupHighlightAll.Create(ASynEdit: TSynEditBase); +begin + inherited Create(ASynEdit); + FSearch := TSynEditSearch.Create; + FSearchString:=''; + FSearchOptions := []; +end; + +destructor TSynEditMarkupHighlightAll.Destroy; +begin + inherited Destroy; + FreeAndNil(FSearch); end; -*) { TSynMarkupHighAllPosList } @@ -282,7 +1443,7 @@ begin Result := (l+h) div 2; end; if (FMatches[Result].EndPoint.y < ALine) then - Result := -1; + inc(Result); end; function TSynMarkupHighAllMatchList.IndexOfLastMatchForLine(ALine: Integer): Integer; @@ -302,7 +1463,7 @@ begin Result := (l+h) div 2; end; if (FMatches[Result].StartPoint.y > ALine) then - Result := -1; + dec(Result); end; procedure TSynMarkupHighAllMatchList.Delete(AIndex: Integer; ACount: Integer); @@ -395,31 +1556,28 @@ begin end; -{ TSynEditMarkupHighlightAll } +{ TSynEditMarkupHighlightAllBase } -constructor TSynEditMarkupHighlightAll.Create(ASynEdit : TSynEditBase); +constructor TSynEditMarkupHighlightAllBase.Create(ASynEdit : TSynEditBase); begin inherited Create(ASynEdit); fStartPoint.y := -1; FSearchedEnd.y := -1; - fSearch := TSynEditSearch.Create; fMatches := TSynMarkupHighAllMatchList.Create; - fSearchString:=''; FFirstInvalidLine := 1; FLastInvalidLine := MaxInt; FHideSingleMatch := False; FMarkupEnabled := MarkupInfo.IsEnabled; end; -destructor TSynEditMarkupHighlightAll.Destroy; +destructor TSynEditMarkupHighlightAllBase.Destroy; begin FoldView := nil; - FreeAndNil(fSearch); FreeAndNil(fMatches); inherited Destroy; end; -procedure TSynEditMarkupHighlightAll.IncPaintLock; +procedure TSynEditMarkupHighlightAllBase.IncPaintLock; begin if FPaintLock = 0 then begin FNeedValidatePaint := False; @@ -428,48 +1586,26 @@ begin inherited IncPaintLock; end; -procedure TSynEditMarkupHighlightAll.DecPaintLock; +procedure TSynEditMarkupHighlightAllBase.DecPaintLock; begin inherited DecPaintLock; if (FPaintLock = 0) and FNeedValidate then ValidateMatches(not FNeedValidatePaint); end; -procedure TSynEditMarkupHighlightAll.SetSearchOptions(const AValue : TSynSearchOptions); -begin - if fSearchOptions = AValue then exit; - fSearchOptions := AValue; - FSearchStringMaxLines := -1; - Invalidate; -end; - -procedure TSynEditMarkupHighlightAll.SetSearchString(const AValue : String); -begin - if fSearchString = AValue then exit; - fSearchString := AValue; - FSearchStringMaxLines := -1; - //DebugLnEnter(['TSynEditMarkupHighlightAll.SetSearchString ', fSearchString]); - Invalidate; // bad if options and string search at the same time *and* string is <> '' - //DebugLnExit(['TSynEditMarkupHighlightAll.SetSearchString ']); -end; - -procedure TSynEditMarkupHighlightAll.DoTopLineChanged(OldTopLine : Integer); +procedure TSynEditMarkupHighlightAllBase.DoTopLineChanged(OldTopLine : Integer); begin // {TODO: Only do a partial search on the new area} - //DebugLnEnter(['TSynEditMarkupHighlightAll.DoTopLineChanged ',FSearchString]); ValidateMatches(True); - //DebugLnExit(['TSynEditMarkupHighlightAll.DoTopLineChanged ']); end; -procedure TSynEditMarkupHighlightAll.DoLinesInWindoChanged(OldLinesInWindow : Integer); +procedure TSynEditMarkupHighlightAllBase.DoLinesInWindoChanged(OldLinesInWindow : Integer); begin // {TODO: Only do a partial search on the new area} - //DebugLnEnter(['TSynEditMarkupHighlightAll.DoLinesInWindoChanged ',FSearchString]); ValidateMatches(True); - //DebugLnExit(['TSynEditMarkupHighlightAll.DoLinesInWindoChanged ']); end; -procedure TSynEditMarkupHighlightAll.DoMarkupChanged(AMarkup : TSynSelectedColor); +procedure TSynEditMarkupHighlightAllBase.DoMarkupChanged(AMarkup : TSynSelectedColor); begin If (not FMarkupEnabled) and MarkupInfo.IsEnabled then Invalidate @@ -478,22 +1614,12 @@ begin FMarkupEnabled := MarkupInfo.IsEnabled; end; -procedure TSynEditMarkupHighlightAll.FindInitialize; -begin - fSearch.Pattern := fSearchString; - fSearch.Sensitive := ssoMatchCase in fSearchOptions; - fSearch.Whole := ssoWholeWord in fSearchOptions; - fSearch.RegularExpressions := ssoRegExpr in fSearchOptions; - fSearch.RegExprMultiLine := ssoRegExprMultiLine in fSearchOptions; - fSearch.Backwards := False; -end; - -function TSynEditMarkupHighlightAll.GetMatchCount: Integer; +function TSynEditMarkupHighlightAllBase.GetMatchCount: Integer; begin Result := fMatches.Count; end; -procedure TSynEditMarkupHighlightAll.SetFoldView(AValue: TSynEditFoldedView); +procedure TSynEditMarkupHighlightAllBase.SetFoldView(AValue: TSynEditFoldedView); begin if FFoldView = AValue then Exit; @@ -506,31 +1632,7 @@ begin FFoldView.AddFoldChangedHandler(@DoFoldChanged); end; -function TSynEditMarkupHighlightAll.FindMatches(AStartPoint, AEndPoint: TPoint; - var AIndex: Integer; AStopAfterLine: Integer; ABackward: Boolean): TPoint; -var - ptFoundStart, ptFoundEnd: TPoint; -begin - //debugln(['FindMatches IDX=', AIndex,' # ',dbgs(AStartPoint),' - ',dbgs(AEndPoint), ' AStopAfterLine=',AStopAfterLine, ' cnt=',FMatches.Count, ' Back=', dbgs(ABackward)]); - fSearch.Backwards := ABackward; - While (true) do begin - if not fSearch.FindNextOne(Lines, AStartPoint, AEndPoint, ptFoundStart, ptFoundEnd) - then break; - AStartPoint := ptFoundEnd; - - FMatches.Insert(AIndex, ptFoundStart, ptFoundEnd); - inc(AIndex); // BAckward learch needs final index to point to last inserted (currently support only find ONE) - - if (AStopAfterLine >= 0) and (ptFoundStart.Y > AStopAfterLine) then begin - AEndPoint := ptFoundEnd; - break; - end; - end; - Result := AEndPoint; - //debugln(['FindMatches IDX=', AIndex, ' ## ',dbgs(Result)]); -end; - -procedure TSynEditMarkupHighlightAll.SetHideSingleMatch(AValue: Boolean); +procedure TSynEditMarkupHighlightAllBase.SetHideSingleMatch(AValue: Boolean); begin if FHideSingleMatch = AValue then Exit; FHideSingleMatch := AValue; @@ -542,49 +1644,15 @@ begin SendLineInvalidation; // Show the existing match end; -procedure TSynEditMarkupHighlightAll.DoFoldChanged(aLine: Integer); +procedure TSynEditMarkupHighlightAllBase.DoFoldChanged(aLine: Integer); begin InvalidateLines(aLine+1, MaxInt, True); end; -function TSynEditMarkupHighlightAll.SearchStringMaxLines: Integer; +procedure TSynEditMarkupHighlightAllBase.ValidateMatches(SkipPaint: Boolean); var - i, j: Integer; -begin - Result := FSearchStringMaxLines; - if Result > 0 then - exit; - - if (fSearchOptions * [ssoRegExpr, ssoRegExprMultiLine] = []) - then begin - // can not wrap around lines - j := 1; - i := Length(fSearchString); - while i > 0 do begin - if fSearchString[i] = #13 then begin - inc(j); - if (i > 1) and (fSearchString[i-1] = #10) then dec(i); // skip alternating - end - else - if fSearchString[i] = #10 then begin - inc(j); - if (i > 1) and (fSearchString[i-1] = #13) then dec(i); // skip alternating - end; - dec(i); - end; - FSearchStringMaxLines := j; - end - else begin - if (fSearchOptions * [ssoRegExpr, ssoRegExprMultiLine] = [ssoRegExpr]) then - FSearchStringMaxLines := 1 // Only ssoRegExprMultiLine can expand accross lines (actually \n\r should anymay...) - else - FSearchStringMaxLines := 0; // Unknown - end; - - Result := FSearchStringMaxLines; -end; - -procedure TSynEditMarkupHighlightAll.ValidateMatches(SkipPaint: Boolean); + LastLine : Integer; // Last visible + UnsentLineInvalidation: Integer; function IsPosValid(APos: TPoint): Boolean; // Check if point is in invalid range begin @@ -594,10 +1662,23 @@ procedure TSynEditMarkupHighlightAll.ValidateMatches(SkipPaint: Boolean); ); end; + function HasInvalidationBetween(ARangeStart, ARangeEnd: TPoint): Boolean; // Check if point is in invalid range + begin + Result := + ((FFirstInvalidLine >= ARangeStart.y) and (FFirstInvalidLine <= ARangeEnd.y)) or + ((FLastInvalidLine >= ARangeStart.y) and (FLastInvalidLine <= ARangeEnd.y)); + end; + function IsStartAtMatch0: Boolean; // Check if FStartPoint = FMatches[0] begin Result := (FMatches.Count > 0) and - (FStartPoint.y = FMatches.EndPoint[0].y)and (FStartPoint.x = FMatches.EndPoint[0].x); + (FStartPoint.y = FMatches.StartPoint[0].y)and (FStartPoint.x = FMatches.StartPoint[0].x); + end; + + function IsEndAtMatch(APoint: TPoint): Boolean; + begin + Result := (FMatches.Count > 0) and + (APoint.y = FMatches.EndPoint[FMatches.Count].y)and (APoint.x = FMatches.EndPoint[FMatches.Count].x); end; function AdjustedSearchStrMaxLines: Integer; @@ -606,24 +1687,41 @@ procedure TSynEditMarkupHighlightAll.ValidateMatches(SkipPaint: Boolean); if Result < 0 then Result := SEARCH_START_OFFS; end; -var - LastLine : Integer; // Last visible + procedure MaybeSendLineInvalidation(AFirstIndex, ALastIndex: Integer); + begin + if SkipPaint or (ALastIndex < AFirstIndex) then + exit; + if HideSingleMatch and (FMatches.Count = 1) then begin + assert((UnsentLineInvalidation < 0) and (AFirstIndex = 0) and (ALastIndex=0), 'UnsentLineInvalidation < 0'); + UnsentLineInvalidation := AFirstIndex; + exit; + end; + + SendLineInvalidation(AFirstIndex, ALastIndex); + if UnsentLineInvalidation >= 0 then + SendLineInvalidation(UnsentLineInvalidation, UnsentLineInvalidation); + UnsentLineInvalidation := -1; + end; procedure MaybeDropOldMatches; var Idx: Integer; begin // remove matches, that are too far off the current visible area - if FMatches.Count > MATCHES_CLEAN_CNT_THRESHOLD then begin + if (FMatches.Count > MATCHES_CLEAN_CNT_THRESHOLD) then begin if TopLine - FMatches.EndPoint[0].y > MATCHES_CLEAN_LINE_THRESHOLD then begin Idx := FMatches.IndexOfFirstMatchForLine(TopLine - MATCHES_CLEAN_LINE_KEEP) - 1; FMatches.Delete(0, Idx); - FStartPoint.y := -1; + if FMatches.Count > 0 + then FStartPoint := FMatches.StartPoint[0] + else FStartPoint.y := -1; end; if FMatches.StartPoint[FMatches.Count-1].y - LastLine > MATCHES_CLEAN_LINE_THRESHOLD then begin Idx := FMatches.IndexOfLastMatchForLine(LastLine + MATCHES_CLEAN_LINE_KEEP) + 1; FMatches.Delete(Idx, FMatches.Count - Idx); - FSearchedEnd.y := -1; + if FMatches.Count > 0 + then FSearchedEnd := FMatches.EndPoint[FMatches.Count-1] + else FSearchedEnd.y := -1; end; end; end; @@ -638,18 +1736,12 @@ var if (FFirstInvalidLine > 0) or (FLastInvalidLine > 0) then begin FirstInvalIdx := FMatches.IndexOfFirstMatchForLine(FFirstInvalidLine); LastInvalIdx := FMatches.IndexOfLastMatchForLine(FLastInvalidLine); - if (FirstInvalIdx >= 0) xor (LastInvalIdx >= 0) then begin - if FirstInvalIdx < 0 then - FirstInvalIdx := 0; - if LastInvalIdx < 0 then - LastInvalIdx := FMatches.Count - 1; - end; - if FirstInvalIdx >= 0 then begin + if (FirstInvalIdx >= 0) and (FirstInvalIdx <= LastInvalIdx) then begin if (not SkipPaint) and HasVisibleMatch then SendLineInvalidation(FirstInvalIdx, LastInvalIdx); FMatches.Delete(FirstInvalIdx, LastInvalIdx-FirstInvalIdx+1); - if FirstInvalIdx >= FMatches.Count then - FirstInvalIdx := -1; + if FirstInvalIdx > FMatches.Count then + FirstInvalIdx := FMatches.Count; end; end; Result := FirstInvalIdx; @@ -657,19 +1749,19 @@ var function FindStartPoint(var AFirstKeptValidIdx: Integer): Boolean; var - Idx: Integer; + Idx : Integer; begin Result := False; // No Gap at start to fill if (FMatches.Count > 0) and (FMatches.StartPoint[0].y < TopLine) then begin // New StartPoint from existing matches + Result := True; FStartPoint := FMatches.StartPoint[0]; if AFirstKeptValidIdx = 0 then AFirstKeptValidIdx := -1; end else begin - Result := True; if SearchStringMaxLines > 0 then // New StartPoint at fixed offset FStartPoint := Point(1, TopLine - (SearchStringMaxLines - 1)) @@ -680,24 +1772,107 @@ var Point(1, TopLine), Idx, 0, True); // stopAfterline=0, do only ONE find if Idx > 0 then begin - FStartPoint := FMatches.EndPoint[0]; + FStartPoint := FMatches.StartPoint[0]; if (AFirstKeptValidIdx >= 0) then inc(AFirstKeptValidIdx, Idx); end else FStartPoint := Point(1, TopLine) // no previous match found end; - //debugln(['ValidateMatches: startpoint ', dbgs(FStartPoint)]); end; end; + procedure MaybeExtendForHideSingle; + var + EndOffsLine: Integer; + Idx: Integer; + begin + // Check, if there is exactly one match in the visible lines + if (not HideSingleMatch) or (Matches.Count <> 1) or + (FMatches.StartPoint[0].y < TopLine) or (FMatches.StartPoint[0].y > LastLine) + then + exit; + + // search 2nd, if HideSingleMatch; + EndOffsLine := min(LastLine+Max(SEARCH_START_OFFS, AdjustedSearchStrMaxLines), Lines.Count); + if EndOffsLine > FSearchedEnd.y then begin + FSearchedEnd.y := FSearchedEnd.y - AdjustedSearchStrMaxLines; + if ComparePoints(FSearchedEnd, FMatches.EndPoint[0]) < 0 then + FSearchedEnd := FMatches.EndPoint[0]; + Idx := 1; + FSearchedEnd := FindMatches(FSearchedEnd, + Point(Length(Lines[EndOffsLine - 1])+1, EndOffsLine), + Idx, LastLine); + SendLineInvalidation; + if Idx > 1 then + exit; + end; + + // search back from start + if FStartPoint.y < TopLine-SEARCH_START_OFFS then + exit; + Idx := 0; + FindMatches(Point(1, Max(1, TopLine-SEARCH_START_OFFS)), FStartPoint, + Idx, 0, True); // stopAfterline=0, do only ONE find // Search backwards + if Idx > 0 then begin + if ComparePoints(FStartPoint, FMatches.StartPoint[0]) = 0 then begin + // bad search: did return endpoint + FMatches.Delete(0); + exit; + end; + FStartPoint := FMatches.StartPoint[0]; + SendLineInvalidation; + end + end; + + procedure FinishValidate; + begin + FFirstInvalidLine := 0; + FLastInvalidLine := 0; + end; + + procedure DoFullSearch(NeedStartPoint: Boolean); + var + dummy: Integer; + EndOffsLine: Integer; + Idx, Idx2: Integer; + p: TPoint; + begin + FMatches.Count := 0; + dummy := -1; + if NeedStartPoint then + FindStartPoint(dummy); + + EndOffsLine := min(LastLine+AdjustedSearchStrMaxLines, Lines.Count); + + if IsStartAtMatch0 then begin + Idx := 1; + p := FMatches.EndPoint[0]; + end else begin + Idx := 0; + p := FStartPoint; + end; + Idx2 := Idx; + FSearchedEnd := FindMatches(p, + Point(Length(Lines[EndOffsLine - 1])+1, EndOffsLine), + Idx, LastLine); + if (not SkipPaint) and (Idx > Idx2) and HasVisibleMatch then + MaybeSendLineInvalidation(0, Idx-1); + + MaybeExtendForHideSingle; + FinishValidate; + end; var - EndOffsLine : Integer; // Stop search (LastLine + Offs) + OldStartPoint, OldEndPoint, GapStartPoint, GapEndPoint: TPoint; + i, j, EndOffsLine : Integer; // Stop search (LastLine + Offs) Idx, Idx2 : Integer; FirstKeptValidIdx: Integer; // The first index, kept after the removed invalidated range - p: TPoint; + p, WorkStartPoint: TPoint; + FindStartPointUsedExistingMatch: Boolean; begin + FNextPosIdx := -1; + FNextPosRow := -1; if (FPaintLock > 0) or (not SynEdit.IsVisible) then begin FNeedValidate := True; if not SkipPaint then @@ -705,9 +1880,8 @@ begin exit; end; FNeedValidate := False; - FNextPosIdx := -1; - if (fSearchString = '') or (not MarkupInfo.IsEnabled) then begin + if (not HasSearchData) or (not MarkupInfo.IsEnabled) then begin if (not SkipPaint) and (fMatches.Count > 0) then SendLineInvalidation; fMatches.Count := 0; @@ -715,150 +1889,228 @@ begin end; LastLine := ScreenRowToRow(LinesInWindow+1); + UnsentLineInvalidation := -1; MaybeDropOldMatches; FirstKeptValidIdx := DeleteInvalidMatches; - //DebugLnEnter(['>>> ValidateMatches ', FFirstInvalidLine, ' - ',FLastInvalidLine, ' Cnt=',FMatches.Count, ' Idx: ', FirstKeptValidIdx, ' ',SynEdit.Name, ' # ',fSearchString]); + //DebugLnEnter(['>>> ValidateMatches ', FFirstInvalidLine, '-',FLastInvalidLine, ' 1stKeepIdx: ', FirstKeptValidIdx, ' __Cnt=',FMatches.Count, '__ StartP=',dbgs(FStartPoint), ' SearchedToP=', dbgs(FSearchedEnd), ' -- ', SynEdit.Name,'.',ClassName]); try FindInitialize; + // Get old valid range as OldStartPoint to OldEndPoint + OldStartPoint := FStartPoint; + OldEndPoint := FSearchedEnd; + if not IsPosValid(FSearchedEnd) then FSearchedEnd.y := -1; + if (OldStartPoint.y >= 0) and not IsPosValid(OldStartPoint) then + OldStartPoint := Point(1, + Min(FLastInvalidLine, MaxInt - AdjustedSearchStrMaxLines) + AdjustedSearchStrMaxLines); + if (OldStartPoint.y < 0) and (FMatches.Count > 0) then + OldStartPoint := FMatches.StartPoint[0]; + + if (OldEndPoint.y >= 0) and not IsPosValid(OldEndPoint) then + OldEndPoint := Point(1, FFirstInvalidLine - AdjustedSearchStrMaxLines); + if (OldEndPoint.y < 0) and (FMatches.Count > 0) then + OldEndPoint := FMatches.EndPoint[FMatches.Count]; + + if (OldEndPoint.y <= OldStartPoint.y) or + (OldEndPoint.y < 0) or (OldStartPoint.y < 0) or + (OldStartPoint.y > LastLine + MATCHES_CLEAN_LINE_KEEP) or + (OldEndPoint.y < TopLine - MATCHES_CLEAN_LINE_KEEP) + then begin + DoFullSearch(True); + exit; + end; + + // Find the minimum gap that needs to be cecalculated for invalidated lines + GapStartPoint.y := -1; + GapEndPoint.y := -1; + if FFirstInvalidLine > 0 then begin + i := AdjustedSearchStrMaxLines; + + GapStartPoint := point(1, Max(1, FFirstInvalidLine - i)); + if (FirstKeptValidIdx > 0) and + (ComparePoints(GapStartPoint, FMatches.EndPoint[FirstKeptValidIdx-1]) < 0) + then + GapStartPoint := FMatches.EndPoint[FirstKeptValidIdx-1]; // GapStartPoint is before known good point + + j := Min(FLastInvalidLine, FLastInvalidLine-i) + i; + GapEndPoint := point(length(SynEdit.Lines[j-1])+1, j); + if (FirstKeptValidIdx >= 0) and (FirstKeptValidIdx < FMatches.Count) and + (ComparePoints(GapEndPoint, FMatches.EndPoint[FirstKeptValidIdx]) > 0) + then + GapEndPoint := FMatches.EndPoint[FirstKeptValidIdx]; // GapEndPoint is after known good point + + // Merge ranges (all points are valid / y >= 0) + if (ComparePoints(GapEndPoint, OldStartPoint) <= 0) or + (ComparePoints(OldEndPoint, GapStartPoint) <= 0) + then begin + // gap outside valid range + GapStartPoint.y := -1; + GapEndPoint.y := -1; + end + else + if (ComparePoints(OldStartPoint, GapStartPoint) >= 0) then begin + // gap starts before valid range, move start point + OldStartPoint := GapEndPoint; + GapStartPoint.y := -1; + GapEndPoint.y := -1; + end + else + if (ComparePoints(OldEndPoint, GapEndPoint) <= 0) then begin + // gap ends after valid range, move end point + OldEndPoint := GapStartPoint; + GapStartPoint.y := -1; + GapEndPoint.y := -1; + end; + + if (OldEndPoint.y <= OldStartPoint.y) or + (OldEndPoint.y < 0) or (OldStartPoint.y < 0) + then begin + DoFullSearch(True); + exit; + end; + end; + + // There is some valid range + // There may be a gap (the gap needs to be inserted at FirstKeptValidIdx) + //DebugLn(['valid: ',dbgs(OldStartPoint),' - ',dbgs(OldEndPoint), ' gap: ',dbgs(GapStartPoint),' - ',dbgs(GapEndPoint)]); + if not ( IsPosValid(FStartPoint) and ( (IsStartAtMatch0 and (FStartPoint.y < TopLine)) or - (FStartPoint.y < TopLine - AdjustedSearchStrMaxLines) or - ((FStartPoint.y = TopLine - AdjustedSearchStrMaxLines) and (FStartPoint.x = 1)) + ( ( (FStartPoint.y < TopLine - AdjustedSearchStrMaxLines) or + ((FStartPoint.y = TopLine - AdjustedSearchStrMaxLines) and (FStartPoint.x = 1)) + ) and + (FStartPoint.y > TopLine - Max(MATCHES_CLEAN_LINE_THRESHOLD, 2*AdjustedSearchStrMaxLines) ) + ) ) ) then begin - if FindStartPoint(FirstKeptValidIdx) - then begin - // Maybe created a gap at start - if IsStartAtMatch0 - then Idx := 1 - else Idx := 0; - if (FMatches.Count > Idx) then begin - if FMatches.StartPoint[Idx].y > LastLine+SEARCH_START_OFFS then begin - // New search has smaller range than gap - FMatches.Delete(Idx, FMatches.Count - Idx); - FSearchedEnd.y := -1; + FindStartPointUsedExistingMatch := FindStartPoint(FirstKeptValidIdx); + + //, existing point must be in valid range, otherwise: + if not FindStartPointUsedExistingMatch then begin + if IsStartAtMatch0 then begin + Idx := 1; + WorkStartPoint := FMatches.EndPoint[0]; + end else begin + Idx := 0; + WorkStartPoint := FStartPoint; + end; + + if ComparePoints(WorkStartPoint, OldEndPoint) >= 1 then begin + // Behind valid range + DoFullSearch(False); + exit; + end; + + if ComparePoints(WorkStartPoint, OldStartPoint) < 1 then begin + // Gap before valid range + if OldStartPoint.y > LastLine+SEARCH_START_OFFS then begin + // Delete all, except StartPoint: New search has smaller range than gap + DoFullSearch(False); + exit; end else begin // *** Fill gap at start Idx2 := Idx; - FindMatches(FStartPoint, FMatches.StartPoint[Idx], Idx); - // We already had at least one match, so now we must have 2 or more and can ignore HideSingleMatch - if (not SkipPaint) and (Idx > Idx2) then - SendLineInvalidation(Idx2, Idx-1); - if FirstKeptValidIdx = Idx2 then - FirstKeptValidIdx := -1; + FindMatches(WorkStartPoint, OldStartPoint, Idx); + //WorkStartPoint := OldStartPoint; + if (not SkipPaint) and (Idx > Idx2) then // TODO: avoid, if only 1 and 1 to hide + MaybeSendLineInvalidation(Idx2, Idx-1); + if (FirstKeptValidIdx >= 0) and (Idx > Idx2) then + inc(FirstKeptValidIdx, Idx-Idx2); end; end; - if (FirstKeptValidIdx >= 0) and (Idx > Idx2) then - inc(FirstKeptValidIdx, Idx-Idx2); + end; end; + FSearchedEnd := OldEndPoint; - if FMatches.Count = 0 then begin - // *** Complete search - EndOffsLine := min(LastLine+AdjustedSearchStrMaxLines, Lines.Count); - if (FSearchedEnd.y < 0) or (EndOffsLine > FSearchedEnd.y) then begin - if HideSingleMatch then - EndOffsLine := min(LastLine+SEARCH_START_OFFS, Lines.Count); - Idx := 0; - FSearchedEnd := FindMatches(FStartPoint, Point(Length(Lines[EndOffsLine - 1]), EndOffsLine), - Idx, LastLine); - if (not SkipPaint) and (Idx > 0) and HasVisibleMatch then - SendLineInvalidation(0, Idx-1); - end; - end - else begin - - if (FirstKeptValidIdx >= 0) and (FirstKeptValidIdx < FMatches.Count) then begin - // Gap from invalidation - if FMatches.StartPoint[Idx].y > LastLine+SEARCH_START_OFFS then begin - // New search (extend end) has smaller range than gap - FMatches.Delete(Idx, FMatches.Count - Idx); - end - else begin - // *** Fill the gap created by invalidation - Idx := FirstKeptValidIdx; // actually first valid now - if Idx > 0 then - FindMatches(FMatches.EndPoint[Idx-1], FMatches.StartPoint[Idx], Idx) - else - FindMatches(FStartPoint, FMatches.StartPoint[Idx], Idx); - // We already had at least one match, so now we must have 2 or more and can ignore HideSingleMatch - if (not SkipPaint) and (Idx > FirstKeptValidIdx) then - SendLineInvalidation(FirstKeptValidIdx, Idx-1); - end; + // Search for the Gap + if (GapStartPoint.y >= 0) then begin + Assert((FirstKeptValidIdx >= 0) or (FMatches.Count = 0), 'FirstKeptValidIdx > 0'); + if FirstKeptValidIdx < 0 then + FirstKeptValidIdx := 0; + if (GapStartPoint.y > LastLine) and + ((not HideSingleMatch) or (FirstKeptValidIdx > 1)) + then begin + // no need to search, done with visible area + FMatches.Delete(FirstKeptValidIdx, FMatches.Count); + FSearchedEnd := GapStartPoint; + FinishValidate; + exit; end; - if FMatches.EndPoint[FMatches.Count - 1].y <= LastLine then begin - // *** extend at end - if HideSingleMatch and (FMatches.Count = 1) then - EndOffsLine := min(LastLine+SEARCH_START_OFFS, Lines.Count) // Search full range, might find a 2nd match - else - EndOffsLine := min(LastLine+AdjustedSearchStrMaxLines, Lines.Count); // Search only for visible new matches - if (FSearchedEnd.y < 0) or (EndOffsLine > FSearchedEnd.y) then begin - Idx := FMatches.Count; - Idx2 := Idx; - p := FMatches.EndPoint[Idx-1]; - if (FSearchedEnd.y - AdjustedSearchStrMaxLines > p.y) then - p := point(1, FSearchedEnd.y - AdjustedSearchStrMaxLines); - FSearchedEnd := FindMatches(p, Point(Length(Lines[EndOffsLine - 1]), EndOffsLine), - Idx, LastLine); - if (not SkipPaint) and (Idx > Idx2) and HasVisibleMatch then - SendLineInvalidation(Idx2, Idx-1); - end; - end; + Idx := FirstKeptValidIdx; + GapStartPoint := FindMatches(GapStartPoint, GapEndPoint, Idx, LastLine); + if (ComparePoints(GapStartPoint, GapEndPoint) < 0) and + HideSingleMatch and (FirstKeptValidIdx < 2) + then + GapStartPoint := FindMatches(GapStartPoint, GapEndPoint, Idx, LastLine); + + if (not SkipPaint) and (Idx > FirstKeptValidIdx) then // TODO: avoid, if only 1 and 1 to hide + MaybeSendLineInvalidation(FirstKeptValidIdx, Idx-1); + + if (ComparePoints(GapStartPoint, GapEndPoint) < 0) and + ((not HideSingleMatch) or (FirstKeptValidIdx > 1)) + then begin + // searched stopped in gap + assert(GapStartPoint.y >= LastLine, 'GapStartPoint.y >= LastLine'); + FSearchedEnd := GapStartPoint; + FinishValidate; + exit; + end; end; - if HideSingleMatch and (FMatches.Count = 1) and - (FMatches.StartPoint[0].y <= LastLine) and (FStartPoint.y > TopLine - SEARCH_START_OFFS) - then begin - // A single match in visible lines is hidden, check for 2nd match before FStartPoint - Idx := 0; - FindMatches(Point(1, Max(1, TopLine-SEARCH_START_OFFS)), FStartPoint, - Idx, 0, True); // stopAfterline=0, do only ONE find // Search backwards - if Idx > 0 then begin - FStartPoint := FMatches.EndPoint[0]; - SendLineInvalidation; - end + // Check at end + if (OldEndPoint.y <= LastLine) then begin + EndOffsLine := min(LastLine+AdjustedSearchStrMaxLines, Lines.Count); // Search only for visible new matches + Idx := FMatches.Count; + Idx2 := Idx; + OldEndPoint.y := OldEndPoint.y - AdjustedSearchStrMaxLines; + if (FMatches.Count > 0) and (ComparePoints(OldEndPoint, FMatches.EndPoint[Idx-1]) < 0) then + OldEndPoint := FMatches.EndPoint[Idx-1]; + p := Point(Length(Lines[EndOffsLine - 1])+1, EndOffsLine); + if ComparePoints(OldEndPoint, p) < 0 then begin + FSearchedEnd := FindMatches(OldEndPoint, p, Idx, LastLine); + if (not SkipPaint) and (Idx > Idx2) and HasVisibleMatch then + MaybeSendLineInvalidation(Idx2, Idx-1); + end; end; - - //DebugLnExit(['<<< ValidateMatches Cnt=',FMatches.Count]) //; - FFirstInvalidLine := 0; - FLastInvalidLine := 0; - FNextPosRow := -1; + MaybeExtendForHideSingle; + FinishValidate; + //finally DebugLnExit([' < ValidateMatches Cnt=',FMatches.Count, ' <<< # ', dbgs(FStartPoint), ' - ', dbgs(FSearchedEnd)]); end; end; -procedure TSynEditMarkupHighlightAll.DoTextChanged(StartLine, EndLine, +procedure TSynEditMarkupHighlightAllBase.DoTextChanged(StartLine, EndLine, ACountDiff: Integer); begin - if (fSearchString = '') then exit; + if (not HasSearchData) then exit; if ACountDiff = 0 then InvalidateLines(StartLine, EndLine+1) else InvalidateLines(StartLine, MaxInt); // LineCount changed end; -procedure TSynEditMarkupHighlightAll.DoVisibleChanged(AVisible: Boolean); +procedure TSynEditMarkupHighlightAllBase.DoVisibleChanged(AVisible: Boolean); begin inherited DoVisibleChanged(AVisible); if FNeedValidate and SynEdit.IsVisible then ValidateMatches(True); end; -function TSynEditMarkupHighlightAll.HasVisibleMatch: Boolean; +function TSynEditMarkupHighlightAllBase.HasVisibleMatch: Boolean; begin Result := ( HideSingleMatch and (FMatches.Count > 1) ) or ( (not HideSingleMatch) and (FMatches.Count > 0) ); end; -procedure TSynEditMarkupHighlightAll.InvalidateLines(AFirstLine: Integer; +procedure TSynEditMarkupHighlightAllBase.InvalidateLines(AFirstLine: Integer; ALastLine: Integer; SkipPaint: Boolean); begin if AFirstLine < 1 then @@ -882,7 +2134,7 @@ begin ValidateMatches(SkipPaint); end; -procedure TSynEditMarkupHighlightAll.SendLineInvalidation(AFirstIndex: Integer; +procedure TSynEditMarkupHighlightAllBase.SendLineInvalidation(AFirstIndex: Integer; ALastIndex: Integer); var Pos: Integer; @@ -918,7 +2170,7 @@ begin InvalidateSynLines(Line1, Line2); end; -procedure TSynEditMarkupHighlightAll.PrepareMarkupForRow(aRow: Integer); +procedure TSynEditMarkupHighlightAllBase.PrepareMarkupForRow(aRow: Integer); begin if (FNextPosRow > 0) and (aRow > FNextPosRow) and ( (FNextPosIdx = -2) or // No match after FNextPosRow @@ -934,20 +2186,26 @@ begin FNextPosRow := aRow; FNextPosIdx := FMatches.IndexOfFirstMatchForLine(aRow) * 2; - if FNextPosIdx < 0 then + if (FNextPosIdx < 0) or (FNextPosIdx >= FMatches.PointCount) then exit; if (FMatches.Point[FNextPosIdx].y < aRow) then inc(FNextPosIdx); // Use EndPoint end; -function TSynEditMarkupHighlightAll.GetMarkupAttributeAtRowCol(const aRow: Integer; +procedure TSynEditMarkupHighlightAllBase.EndMarkup; +begin + inherited EndMarkup; + FNextPosRow := -1; +end; + +function TSynEditMarkupHighlightAllBase.GetMarkupAttributeAtRowCol(const aRow: Integer; const aStartCol: TLazSynDisplayTokenBound; const AnRtlInfo: TLazSynDisplayRtlInfo): TSynSelectedColor; var pos, s, e: Integer; begin result := nil; - if (fSearchString = '') then - exit; + if (not HasSearchData) then + exit; if (HideSingleMatch and (fMatches.Count <= 1)) or (aRow <> FNextPosRow) or (FNextPosIdx < 0) then @@ -959,7 +2217,7 @@ begin do inc(FNextPosIdx); - if FNextPosIdx = fMatches.PointCount // last point was EndPoint => no markup + if FNextPosIdx >= fMatches.PointCount // last point was EndPoint => no markup then exit; pos := FNextPosIdx - 1; @@ -975,7 +2233,6 @@ begin if (pos and 1)= 1 // the Point is a EndPoint => Outside Match then exit; - //debugLN('+++>MARUP *ON* ',dbgs(@result),' / ',dbgs(ARow), ' at index ', dbgs(FNextPosIdx)); if fMatches.Point[pos].y < aRow then s := -1 else @@ -988,13 +2245,13 @@ begin Result := MarkupInfo; end; -procedure TSynEditMarkupHighlightAll.GetNextMarkupColAfterRowCol(const aRow: Integer; +procedure TSynEditMarkupHighlightAllBase.GetNextMarkupColAfterRowCol(const aRow: Integer; const aStartCol: TLazSynDisplayTokenBound; const AnRtlInfo: TLazSynDisplayRtlInfo; out ANextPhys, ANextLog: Integer); begin ANextLog := -1; ANextPhys := -1; - if (fSearchString = '') then + if (not HasSearchData) then exit; if (HideSingleMatch and (fMatches.Count <= 1)) or (aRow <> FNextPosRow) or (FNextPosIdx < 0) or @@ -1008,22 +2265,23 @@ begin do inc(FNextPosIdx); - if FNextPosIdx = fMatches.PointCount - then exit; - if fMatches.Point[FNextPosIdx].y <> aRow - then exit; + if FNextPosIdx >= fMatches.PointCount then + exit; + if fMatches.Point[FNextPosIdx].y <> aRow then + exit; ANextLog := fMatches.Point[FNextPosIdx].x; - //debugLN('--->NEXT POS ',dbgs(ANextLog),' / ',dbgs(ARow), ' at index ', dbgs(Pos)); end; -procedure TSynEditMarkupHighlightAll.Invalidate(SkipPaint: Boolean); +procedure TSynEditMarkupHighlightAllBase.Invalidate(SkipPaint: Boolean); begin if not SkipPaint then SendLineInvalidation; FStartPoint.y := -1; FSearchedEnd.y := -1; FMatches.Count := 0; + FFirstInvalidLine := 1; + FLastInvalidLine := MaxInt; ValidateMatches(SkipPaint); end; @@ -1039,10 +2297,9 @@ begin RestartTimer; end; -procedure TSynEditMarkupHighlightAllCaret.SetSearchString(const AValue: String); +procedure TSynEditMarkupHighlightAllCaret.SearchStringChanged; begin - inherited SetSearchString(AValue); - if AValue = '' then + if SearchString = '' then FLowBound.X := -1; FOldLowBound := FLowBound; FOldUpBound := FUpBound; diff --git a/components/synedit/test/testbase.pas b/components/synedit/test/testbase.pas index ecc909b04a..1abdaf31bb 100644 --- a/components/synedit/test/testbase.pas +++ b/components/synedit/test/testbase.pas @@ -7,7 +7,7 @@ interface uses Classes, SysUtils, Forms, fpcunit, SynEdit, LCLType, LCLProc, math, - SynEditTypes, SynEditPointClasses, SynEditKeyCmds, LazSynTextArea, Clipbrd; + SynEditTypes, SynEditPointClasses, SynEditKeyCmds, LazSynTextArea, SynEditMarkup, Clipbrd; type @@ -23,6 +23,8 @@ type { TTestSynEdit } TTestSynEdit = class(TSynEdit) + private + function TestGetMarkupMgr: TSynEditMarkupManager; public procedure TestKeyPress(Key: Word; Shift: TShiftState); procedure TestTypeText(ALogCaretX, ALogCaretY: Integer; Input: String; WithSimulatedPaint: Boolean = False); @@ -38,6 +40,7 @@ type property TextView; // foldedview property CaretObj: TSynEditCaret read GetCaretObj; property TextArea: TLazSynTextArea read FTextArea; + property MarkupMgr: TSynEditMarkupManager read TestGetMarkupMgr; end; { TTestBase } @@ -135,6 +138,11 @@ end; { TTestSynEdit } +function TTestSynEdit.TestGetMarkupMgr: TSynEditMarkupManager; +begin + Result := TSynEditMarkupManager(inherited MarkupMgr); +end; + procedure TTestSynEdit.TestKeyPress(Key: Word; Shift: TShiftState); var c: TUTF8Char; diff --git a/components/synedit/test/testmarkuphighall.pas b/components/synedit/test/testmarkuphighall.pas new file mode 100644 index 0000000000..6398a9e381 --- /dev/null +++ b/components/synedit/test/testmarkuphighall.pas @@ -0,0 +1,694 @@ +unit TestMarkupHighAll; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, testregistry, TestBase, TestHighlightPas, Forms, LCLProc, Controls, + Graphics, SynEdit, SynHighlighterPas, SynEditMarkupHighAll; + +type + + { TTestMarkupHighAll } + + TTestMarkupHighAll = class(TTestBase) + private + FMatchList: Array of record + p: PChar; + l: Integer; + end; + procedure DoDictMatch(Match: PChar; MatchLen: Integer; var IsMatch: Boolean; + var StopSeach: Boolean); + protected + //procedure SetUp; override; + //procedure TearDown; override; + //procedure ReCreateEdit; reintroduce; + //function TestText1: TStringArray; + published + procedure TestDictionary; + procedure TestValidateMatches; + end; + +implementation + + type + + { TTestSynEditMarkupHighlightAllMulti } + + TTestSynEditMarkupHighlightAllMulti = class(TSynEditMarkupHighlightAllMulti) + private + FScannedLineCount: Integer; + protected + function FindMatches(AStartPoint, AEndPoint: TPoint; var AIndex: Integer; + AStopAfterLine: Integer = - 1; ABackward: Boolean = False): TPoint; override; + public + procedure ResetScannedCount; + property Matches; + property ScannedLineCount: Integer read FScannedLineCount; + end; + +{ TTestSynEditMarkupHighlightAllMulti } + +function TTestSynEditMarkupHighlightAllMulti.FindMatches(AStartPoint, AEndPoint: TPoint; + var AIndex: Integer; AStopAfterLine: Integer; ABackward: Boolean): TPoint; +begin + FScannedLineCount := FScannedLineCount + AEndPoint.y - AStartPoint.y + 1; + Result := inherited FindMatches(AStartPoint, AEndPoint, AIndex, AStopAfterLine, ABackward); +end; + +procedure TTestSynEditMarkupHighlightAllMulti.ResetScannedCount; +begin + FScannedLineCount := 0; +end; + + +{ TTestMarkupHighAll } + +procedure TTestMarkupHighAll.DoDictMatch(Match: PChar; MatchLen: Integer; + var IsMatch: Boolean; var StopSeach: Boolean); +var + i: Integer; +begin + i := length(FMatchList); + SetLength(FMatchList, i+1); +DebugLn([copy(Match, 1, MatchLen)]); + FMatchList[i].p := Match; + FMatchList[i].l := MatchLen; +end; + +procedure TTestMarkupHighAll.TestDictionary; +var + Dict: TSynSearchDictionary; + i, j: Integer; + s: String; +begin + Dict := TSynSearchDictionary.Create; + Dict.Add('debugln',1); + Dict.Add('debuglnenter',2); + Dict.Add('debuglnexit',3); + Dict.Add('dbgout',4); + + Dict.DebugPrint(); + + + Dict.Free; +exit; + + Dict := TSynSearchDictionary.Create; + Dict.Add('Hello', 0); + Dict.Add('hell', 0); + Dict.Add('hatter', 0); + Dict.Add('log', 0); + Dict.Add('lantern', 0); + Dict.Add('terminal', 0); + Dict.Add('all', 0); + Dict.Add('alt', 0); + + + Dict.Search('aallhellxlog', 12, @DoDictMatch); + + //Dict.BuildDictionary; + Dict.DebugPrint(); + + //Randomize; + //Dict.Clear; + //for i := 0 to 5000 do begin + // s := ''; + // for j := 10 to 11+Random(20) do s := s + chr(Random(127)); + // Dict.Add(s); + //end; + //Dict.BuildDictionary; + //Dict.DebugPrint(true); + + + Dict.Free; +end; + +procedure TTestMarkupHighAll.TestValidateMatches; +type + TMatchLoc = record + y1, y2, x1, x2: Integer; + end; +var + M: TTestSynEditMarkupHighlightAllMulti; + + function l(y, x1, x2: Integer) : TMatchLoc; + begin + Result.y1 := y; + Result.x1 := x1; + Result.y2 := y; + Result.x2 := x2; + end; + + procedure StartMatch(Words: Array of string); + var + i: Integer; + begin + SynEdit.BeginUpdate; + M.Clear; + for i := 0 to high(Words) do + M.AddSearchTerm(Words[i]); + SynEdit.EndUpdate; + m.MarkupInfo.Foreground := clRed; + end; + + Procedure TestHasMCount(AName: String; AExpMin: Integer; AExpMax: Integer = -1); + begin + AName := AName + '(CNT)'; + if AExpMax < 0 then begin + AssertEquals(BaseTestName+' '+AName, AExpMin, M.Matches.Count); + end + else begin + AssertTrue(BaseTestName+' '+AName+ '(Min)', AExpMin <= M.Matches.Count); + AssertTrue(BaseTestName+' '+AName+ '(Max)', AExpMax >= M.Matches.Count); + end; + end; + + Procedure TestHasMatches(AName: String; AExp: Array of TMAtchLoc; ExpMusNotExist: Boolean = False); + var + i, j: Integer; + begin + for i := 0 to High(AExp) do begin + j := M.Matches.Count - 1; + while (j >= 0) and + ( (M.Matches.StartPoint[j].y <> AExp[i].y1) or (M.Matches.StartPoint[j].x <> AExp[i].x1) or + (M.Matches.EndPoint[j].y <> AExp[i].y2) or (M.Matches.EndPoint[j].x <> AExp[i].x2) ) + do + dec(j); + AssertEquals(BaseTestName+' '+AName+'('+IntToStr(i)+')', not ExpMusNotExist, j >= 0); + end + end; + + Procedure TestHasMatches(AName: String; AExpCount: Integer; AExp: Array of TMAtchLoc; ExpMusNotExist: Boolean = False); + begin + TestHasMatches(AName, AExp, ExpMusNotExist); + TestHasMCount(AName, AExpCount); + end; + + Procedure TestHasMatches(AName: String; AExpCountMin, AExpCountMax: Integer; AExp: Array of TMAtchLoc; ExpMusNotExist: Boolean = False); + begin + TestHasMatches(AName, AExp, ExpMusNotExist); + TestHasMCount(AName, AExpCountMin, AExpCountMax); + end; + + Procedure TestHasScanCnt(AName: String; AExpMin: Integer; AExpMax: Integer = -1); + begin + AName := AName + '(SCANNED)'; + if AExpMax < 0 then begin + AssertEquals(BaseTestName+' '+AName, AExpMin, M.ScannedLineCount); + end + else begin + AssertTrue(BaseTestName+' '+AName+ '(Min)', AExpMin <= M.ScannedLineCount); + AssertTrue(BaseTestName+' '+AName+ '(Max)', AExpMax >= M.ScannedLineCount); + end; + end; + + procedure SetText(ATopLine: Integer = 1; HideSingle: Boolean = False); + var + i: Integer; + begin + ReCreateEdit; + SynEdit.BeginUpdate; + for i := 1 to 700 do + SynEdit.Lines.Add(' a'+IntToStr(i)+'a b c'+IntToStr(i)+'d'); + SynEdit.Align := alTop; + SynEdit.Height := SynEdit.LineHeight * 40 + SynEdit.LineHeight div 2; + M := TTestSynEditMarkupHighlightAllMulti.Create(SynEdit); + M.HideSingleMatch := HideSingle; + SynEdit.MarkupMgr.AddMarkUp(M); + SynEdit.TopLine := ATopLine; + SynEdit.EndUpdate; + end; + + procedure SetTextAndMatch(ATopLine: Integer; HideSingle: Boolean; + Words: Array of string; + AName: String= ''; AExpMin: Integer = -1; AExpMax: Integer = -1); + begin + SetText(ATopLine, HideSingle); + StartMatch(Words); + if AExpMin >= 0 then + TestHasMCount(AName + ' init', AExpMin, AExpMax); + end; + +var + N: string; + i, j, a, b: integer; +begin + + {%region Searchrange} + PushBaseName('Searchrange'); + PushBaseName('HideSingleMatch=False'); + + N := 'Find match on first line'; + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a250a']); + TestHasMCount (N, 1); + TestHasMatches(N, [l(250, 3, 8)]); + + N := 'Find match on last line'; + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a289a']); + TestHasMCount (N, 1); + TestHasMatches(N, [l(289, 3, 8)]); + + N := 'Find match on last part visible) line'; + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a290a']); + TestHasMCount (N, 1); + TestHasMatches(N, [l(290, 3, 8)]); + + // Before topline + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a249a']); + TestHasMCount ('NOT Found before topline', 0); + + // after lastline + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a291a']); + TestHasMCount ('NOT Found after lastline', 0); + + // first and last + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a250a', 'a290a']); + TestHasMCount ('Found on first and last line', 2); + TestHasMatches('Found on first and last line', [l(250, 3, 8), l(290, 3, 8)]); + + // first and last + before + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a250a', 'a290a', 'a249a']); + TestHasMCount ('Found on first/last (but not before) line', 2); + TestHasMatches('Found on first/last (but not before) line', [l(250, 3, 8), l(290, 3, 8)]); + + // first and last + after + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a250a', 'a290a', 'a291a']); + TestHasMCount ('Found on first/last (but not after) line', 2); + TestHasMatches('Found on first/last (but not after) line', [l(250, 3, 8), l(290, 3, 8)]); + + PopPushBaseName('HideSingleMatch=True'); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a']); + TestHasMCount ('Found on first line', 1); + TestHasMatches('Found on first line', [l(250, 3, 8)]); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a289a']); + TestHasMCount ('Found on last line', 1); + TestHasMatches('Found on last line', [l(289, 3, 8)]); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a290a']); + TestHasMCount ('Found on last (partly) line', 1); + TestHasMatches('Found on last (partly) line', [l(290, 3, 8)]); + + // Before topline + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a249a']); + TestHasMCount ('NOT Found before topline', 0); + + // after lastline + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a291a']); + TestHasMCount ('NOT Found after lastline', 0); + + // first and last + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a290a']); + TestHasMCount ('Found on first and last line', 2); + TestHasMatches('Found on first and last line', [l(250, 3, 8), l(290, 3, 8)]); + + // first and last + before + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a290a', 'a249a']); + TestHasMCount ('Found on first/last (but not before) line', 2); + TestHasMatches('Found on first/last (but not before) line', [l(250, 3, 8), l(290, 3, 8)]); + + // first and last + after + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a290a', 'a291a']); + TestHasMCount ('Found on first/last (but not after) line', 2); + TestHasMatches('Found on first/last (but not after) line', [l(250, 3, 8), l(290, 3, 8)]); + + // extend for HideSingle, before + N := 'Look for 2nd match before startpoint (first match at topline)'; + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a249a']); + TestHasMCount (N, 2); + TestHasMatches(N, [l(250, 3, 8), l(249, 3, 8)]); + + N := 'Look for 2nd match before startpoint (first match at lastline)'; + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a290a', 'a249a']); + TestHasMCount (N, 2); + TestHasMatches(N, [l(290, 3, 8), l(249, 3, 8)]); + + N := 'Look for 2nd match FAR (99l) before startpoint (first match at topline)'; + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a151a']); + TestHasMCount (N, 2); + TestHasMatches(N, [l(250, 3, 8), l(151, 3, 8)]); + + N := 'Look for 2nd match FAR (99l) before startpoint (first match at lastline)'; + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a290a', 'a151a']); + TestHasMCount (N, 2); + TestHasMatches(N, [l(290, 3, 8), l(151, 3, 8)]); + + N := 'Look for 2nd match before startpoint, find ONE of TWO'; + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a200a', 'a210a']); + TestHasMCount (N, 2); + TestHasMatches(N, [l(250, 3, 8), l(210, 3, 8)]); + + // TODO: Not extend too far... + + // extend for HideSingle, after + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a291a']); + TestHasMCount ('Found on first/ext-after line', 2); + TestHasMatches('Found on first/ext-after line', [l(250, 3, 8), l(291, 3, 8)]); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a290a', 'a291a']); + TestHasMCount ('Found on last/ext-after line', 2); + TestHasMatches('Found on last/ext-after line', [l(290, 3, 8), l(291, 3, 8)]); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a250a', 'a389a']); + TestHasMCount ('Found on first/ext-after-99 line', 2); + TestHasMatches('Found on first/ext-after-99 line', [l(250, 3, 8), l(389, 3, 8)]); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a290a', 'a389a']); + TestHasMCount ('Found on last/ext-after-99 line', 2); + TestHasMatches('Found on last/ext-after-99 line', [l(290, 3, 8), l(389, 3, 8)]); + + + PopBaseName; + PopBaseName; + {%endregion} + + {%region Scroll / LinesInWindow} + PushBaseName('Scroll/LinesInWindow'); + PushBaseName('HideSingleMatch=False'); + + + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a249a']); + TestHasMCount ('Not Found before first line', 0); + + M.ResetScannedCount; + SynEdit.TopLine := 251; + TestHasMCount ('Not Found before first line (250=>251)', 0); + TestHasScanCnt('Not Found before first line (250=>251)', 1, 2); // Allow some range + + M.ResetScannedCount; + SynEdit.TopLine := 249; + TestHasMCount ('Found on first line (251=<249', 1); + TestHasMatches('Found on first line (251=>249)', [l(249, 3, 8)]); + TestHasScanCnt('Found on first line (251=>249)', 1, 2); // Allow some range + + + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a291a']); + TestHasMCount ('Not Found after last line', 0); + + M.ResetScannedCount; + SynEdit.TopLine := 249; + TestHasMCount ('Not Found after last line (250=>249)', 0); + TestHasScanCnt('Not Found after last line (250=>249)', 1, 2); // Allow some range + + M.ResetScannedCount; + SynEdit.TopLine := 251; + TestHasMCount ('Found on last line (249=<251', 1); + TestHasMatches('Found on last line (249=>251)', [l(291, 3, 8)]); + TestHasScanCnt('Found on last line (249=>251)', 1, 2); // Allow some range + + + SetText(250); + M.HideSingleMatch := False; + StartMatch(['a291a']); + TestHasMCount ('Not Found after last line', 0); + + M.ResetScannedCount; + SynEdit.Height := SynEdit.LineHeight * 41 + SynEdit.LineHeight div 2; + TestHasMCount ('Found on last line (40=>41', 1); + TestHasMatches('Found on last line (40=>41)', [l(291, 3, 8)]); + TestHasScanCnt('Found on last line (40=>41)', 1, 2); // Allow some range + + + PopPushBaseName('HideSingleMatch=True'); + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a249a', 'a248a']); + TestHasMCount ('Not Found before first line', 0); + + M.ResetScannedCount; + SynEdit.TopLine := 251; + TestHasMCount ('Not Found before first line (250=>251)', 0); + TestHasScanCnt('Not Found before first line (250=>251)', 1, 2); // Allow some range + + M.ResetScannedCount; + SynEdit.TopLine := 249; + TestHasMCount ('Found on first line+ext (251=<249', 2); + TestHasMatches('Found on first line+ext (251=>249)', [l(249, 3, 8), l(248, 3, 8)]); + + + SetText(250); + M.HideSingleMatch := True; + StartMatch(['a291a', 'a292a']); + TestHasMCount ('Not Found after last line', 0); + + M.ResetScannedCount; + SynEdit.TopLine := 249; + TestHasMCount ('Not Found after last line (250=>249)', 0); + TestHasScanCnt('Not Found after last line (250=>249)', 1, 2); // Allow some range + + M.ResetScannedCount; + SynEdit.TopLine := 251; + TestHasMCount ('Found on last line+ext (249=<251', 2); + TestHasMatches('Found on last line+ext (249=>251)', [l(291, 3, 8), l(292, 3, 8)]); + + PopBaseName; + PopBaseName; + {%endregion} + + {%region edit} + PushBaseName('Searchrange'); + //PushBaseName('HideSingleMatch=False'); + + for i := 245 to 295 do begin + if ((i > 259) and (i < 280)) then continue; + + N := 'Edit at '+IntToStr(i)+' / NO match'; + SetTextAndMatch(250, False, ['DontMatchMe'], N+' init/found', 0); + M.ResetScannedCount; + SynEdit.TextBetweenPoints[point(1, i), point(1, i)] := 'X'; + SynEdit.SimulatePaintText; + TestHasMCount (N+' Found after edit', 0); + if (i >= 250) and (i <= 290) + then TestHasScanCnt(N+' Found after edit', 1, 3) + else + if (i < 247) or (i > 293) + then TestHasScanCnt(N+' Found after edit', 0) + else TestHasScanCnt(N+' Found after edit', 0, 3); + + + N := 'Edit (new line) at '+IntToStr(i)+' / NO match'; + SetTextAndMatch(250, False, ['DontMatchMe'], N+' init/found', 0); + M.ResetScannedCount; + SynEdit.TextBetweenPoints[point(1, i), point(1, i)] := LineEnding; + SynEdit.SimulatePaintText; + TestHasMCount (N+' Found after edit', 0); + //if (i >= 250) and (i <= 290) + //then TestHasScanCnt(N+' Found after edit', 1, 3) + //else + //if (i < 247) or (i > 293) + //then TestHasScanCnt(N+' Found after edit', 0) + //else TestHasScanCnt(N+' Found after edit', 0, 3); + + + N := 'Edit (join line) at '+IntToStr(i)+' / NO match'; + SetTextAndMatch(250, False, ['DontMatchMe'], N+' init/found', 0); + M.ResetScannedCount; + SynEdit.TextBetweenPoints[point(10, i), point(1, i+1)] := ''; + SynEdit.SimulatePaintText; + TestHasMCount (N+' Found after edit', 0); + //if (i >= 250) and (i <= 290) + //then TestHasScanCnt(N+' Found after edit', 1, 3) + //else + //if (i < 247) or (i > 293) + //then TestHasScanCnt(N+' Found after edit', 0) + //else TestHasScanCnt(N+' Found after edit', 0, 3); + + end; + + + + for j := 245 to 295 do begin + if ((j > 255) and (j < 270)) or ((j > 270) and (j < 285)) then + continue; + + for i := 245 to 295 do begin + N := 'Edit at '+IntToStr(i)+' / single match at '+IntToStr(j); + SetTextAndMatch(250, False, ['a'+IntToStr(j)+'a']); + if (j >= 250) and (j <= 290) + then TestHasMatches(N+' init/found', 1, [l(j, 3, 8)]) + else TestHasMCount (N+' init/not found', 0); + + M.ResetScannedCount; + SynEdit.TextBetweenPoints[point(1, i), point(1, i)] := 'X'; + SynEdit.SimulatePaintText; + if (j >= 250) and (j <= 290) then begin + if i = j + then TestHasMatches(N+' Found after edit', 1, [l(j, 4, 9)]) + else TestHasMatches(N+' Found after edit', 1, [l(j, 3, 8)]); + end + else + TestHasMCount (N+' still not Found after edit', 0); + + if (i >= 250) and (i <= 290) + then TestHasScanCnt(N+' Found after edit', 1, 3) + else + if (i < 247) or (i > 293) + then TestHasScanCnt(N+' Found after edit', 0) + else TestHasScanCnt(N+' Found after edit', 0, 3); + end; + + + for i := 245 to 295 do begin + N := 'Edit (new line) at '+IntToStr(i)+' / single match at '+IntToStr(j); + SetTextAndMatch(250, False, ['a'+IntToStr(j)+'a']); + if (j >= 250) and (j <= 290) + then TestHasMatches(N+' init/found', 1, [l(j, 3, 8)]) + else TestHasMCount (N+' init/not found', 0); + + M.ResetScannedCount; + SynEdit.BeginUpdate; + SynEdit.TextBetweenPoints[point(1, i), point(1, i)] := LineEnding; + SynEdit.TopLine := 250; + SynEdit.EndUpdate; + SynEdit.SimulatePaintText; + a := j; + if i <= j then inc(a); + if (a >= 250) and (a <= 290) then begin + if i = a + then TestHasMatches(N+' Found after edit', 1, [l(a, 4, 9)]) + else TestHasMatches(N+' Found after edit', 1, [l(a, 3, 8)]); + end + else + TestHasMCount (N+' still not Found after edit', 0); + + //if (i >= 250) and (i <= 290) + //then TestHasScanCnt(N+' Found after edit', 1, 3) + //else + //if (i < 247) or (i > 293) + //then TestHasScanCnt(N+' Found after edit', 0) + //else TestHasScanCnt(N+' Found after edit', 0, 3); + end; + + end; + + + + for j := 0 to 6 do begin + case j of + 0: begin a := 260; b := 270 end; + 1: begin a := 250; b := 270 end; + 2: begin a := 251; b := 270 end; + 3: begin a := 270; b := 288 end; + 4: begin a := 270; b := 289 end; + 5: begin a := 270; b := 290 end; + 6: begin a := 250; b := 290 end; + end; + + for i := 245 to 295 do begin + N := 'Edit at '+IntToStr(i)+' / TWO match at '+IntToStr(a)+', '+IntToStr(b); + SetTextAndMatch(250, False, ['a'+IntToStr(a)+'a', 'a'+IntToStr(b)+'a']); + TestHasMatches(N+' init/found', 2, [l(a, 3, 8), l(b, 3,8)]); + + M.ResetScannedCount; + SynEdit.TextBetweenPoints[point(10, i), point(10, i)] := 'X'; + SynEdit.SimulatePaintText; + TestHasMCount (N+' Found after edit', 2); + TestHasMatches(N+' init/found', [l(a, 3, 8), l(b, 3,8)]); + + if (i >= 250) and (i <= 290) + then TestHasScanCnt(N+' Found after edit', 1, 3) + else + if (i < 247) or (i > 293) + then TestHasScanCnt(N+' Found after edit', 0) + else TestHasScanCnt(N+' Found after edit', 0, 3); + end; + end; + + + N := 'Edit/Topline/LastLine '; + SetTextAndMatch(250, False, ['a265a', 'a275a']); + TestHasMatches(N+' init/found', 2, [l(265, 3, 8), l(275, 3,8)]); + M.ResetScannedCount; + SynEdit.BeginUpdate; + SynEdit.TextBetweenPoints[point(10, i), point(10, i)] := 'X'; + SynEdit.TopLine := 248; // 2 new lines + SynEdit.Height := SynEdit.LineHeight * 44 + SynEdit.LineHeight div 2; // another 2 lines + SynEdit.EndUpdate; + SynEdit.SimulatePaintText; + TestHasMatches(N+' Found after edit', 2, [l(265, 3, 8), l(275, 3,8)]); + TestHasScanCnt(N+' Found after edit', 1, 12); + + + N := 'Edit/Topline/LastLine find new points'; + SetTextAndMatch(250, False, ['a265a', 'a275a', 'a248a', 'a292a']); + TestHasMatches(N+' init/found', 2, [l(265, 3, 8), l(275, 3,8)]); + M.ResetScannedCount; + SynEdit.BeginUpdate; + SynEdit.TextBetweenPoints[point(10, i), point(10, i)] := 'X'; + SynEdit.TopLine := 248; // 2 new lines + SynEdit.Height := SynEdit.LineHeight * 44 + SynEdit.LineHeight div 2; // another 2 lines + SynEdit.EndUpdate; + SynEdit.SimulatePaintText; + TestHasMatches(N+' Found after edit', 4, [l(265, 3, 8), l(275, 3,8), l(248, 3,8), l(292, 3,8)]); + TestHasScanCnt(N+' Found after edit', 1, 12); + + PopBaseName; + {%endregion} + + + +end; + +initialization + + RegisterTest(TTestMarkupHighAll); +end. +