unit ATSynEdit_Finder; {$mode objfpc}{$H+} interface uses SysUtils, Classes, Dialogs, Forms, Math, RegExpr, //must be with {$define Unicode} ATSynEdit, ATSynEdit_Carets, ATStringProc, ATStringProc_TextBuffer; type TATFinderProgress = procedure(Sender: TObject; ACurPos, AMaxPos: integer; var AContinue: boolean) of object; TATFinderFound = procedure(Sender: TObject; APos1, APos2: TPoint) of object; TATFinderConfirmReplace = procedure(Sender: TObject; APos1, APos2: TPoint; AForMany: boolean; var AConfirm, AContinue: boolean) of object; type { TATTextFinder } TATTextFinder = class private FMatchPos: integer; FMatchLen: integer; FStrFind: UnicodeString; FStrReplace: UnicodeString; FStrReplacement: UnicodeString; FOnProgress: TATFinderProgress; FOnBadRegex: TNotifyEvent; function DoCountMatchesRegex(FromPos: integer; AWithEvent: boolean): integer; function DoCountMatchesUsual(FromPos: integer; AWithEvent: boolean): Integer; function DoFindMatchRegex(FromPos: integer; var MatchPos, MatchLen: integer): boolean; function DoFindMatchUsual(FromPos: integer): Integer; function IsMatchUsual(APos: integer): boolean; procedure SetStrFind(const AValue: UnicodeString); procedure SetStrReplace(const AValue: UnicodeString); protected procedure DoOnFound; virtual; public OptBack: boolean; //for non-regex OptWords: boolean; //for non-regex OptCase: boolean; //for regex and usual OptRegex: boolean; OptWrapped: boolean; StrText: UnicodeString; property StrFind: UnicodeString read FStrFind write SetStrFind; property StrReplace: UnicodeString read FStrReplace write SetStrReplace; property StrReplacement: UnicodeString read FStrReplacement; //for regex constructor Create; destructor Destroy; override; function FindMatch(ANext: boolean; ASkipLen: integer; AStartPos: integer): boolean; property MatchPos: integer read FMatchPos; //have meaning if FindMatch returned True property MatchLen: integer read FMatchLen; //too property OnProgress: TATFinderProgress read FOnProgress write FOnProgress; property OnBadRegex: TNotifyEvent read FOnBadRegex write FOnBadRegex; end; type { TATEditorFinder } TATEditorFinder = class(TATTextFinder) private FBuffer: TATStringBuffer; FEditor: TATSynEdit; FSkipLen: integer; FOnFound: TATFinderFound; FOnConfirmReplace: TATFinderConfirmReplace; function DoFindOrReplace_Internal(ANext, AReplace, AForMany: boolean; out AChanged: boolean; AStartPos: integer): boolean; procedure DoFixCaretSelectionDirection; procedure DoReplaceTextInEditor(P1, P2: TPoint); function GetOffsetOfCaret: integer; function GetOffsetStartPos: integer; function GetRegexSkipIncrement: integer; protected procedure DoOnFound; override; public OptFromCaret: boolean; OptConfirmReplace: boolean; constructor Create; destructor Destroy; override; procedure UpdateBuffer; property Editor: TATSynEdit read FEditor write FEditor; property OnFound: TATFinderFound read FOnFound write FOnFound; property OnConfirmReplace: TATFinderConfirmReplace read FOnConfirmReplace write FOnConfirmReplace; function DoFindOrReplace(ANext, AReplace, AForMany: boolean; out AChanged: boolean): boolean; function DoReplaceSelectedMatch: boolean; function DoCountAll(AWithEvent: boolean): integer; function DoReplaceAll: integer; function IsSelectionStartsAtFoundMatch: boolean; end; implementation function IsWordChar(ch: Widechar): boolean; begin Result:= ATStringProc.IsCharWord(ch, ''); end; function SRegexReplaceEscapedTabs(const AStr: string): string; begin Result:= AStr; Result:= StringReplace(Result, '\\', #1, [rfReplaceAll]); Result:= StringReplace(Result, '\t', #9, [rfReplaceAll]); Result:= StringReplace(Result, #1, '\\', [rfReplaceAll]); end; function TATTextFinder.IsMatchUsual(APos: integer): boolean; var LenF, LastPos: integer; begin Result:= false; if StrFind='' then exit; if StrText='' then exit; LenF:= Length(StrFind); LastPos:= Length(StrText)-LenF+1; if OptCase then Result:= CompareMem(@StrFind[1], @StrText[APos], LenF*2) else Result:= UnicodeLowerCase(StrFind) = UnicodeLowerCase(Copy(StrText, APos, LenF)); if Result then if OptWords then Result:= ((APos <= 1) or (not IsWordChar(StrText[APos - 1]))) and ((APos >= LastPos) or (not IsWordChar(StrText[APos + LenF]))); end; procedure TATTextFinder.SetStrFind(const AValue: UnicodeString); begin if FStrFind=AValue then Exit; FStrFind:= AValue; FMatchPos:= -1; FMatchLen:= 0; end; procedure TATTextFinder.SetStrReplace(const AValue: UnicodeString); begin if FStrReplace=AValue then Exit; FStrReplace:= AValue; end; procedure TATTextFinder.DoOnFound; begin // end; function TATTextFinder.DoFindMatchUsual(FromPos: integer): Integer; var LastPos, i: integer; begin Result:= 0; if StrText='' then exit; if StrFind='' then exit; LastPos:= Length(StrText) - Length(StrFind) + 1; if not OptBack then for i:= FromPos to LastPos do begin if IsMatchUsual(i) then begin Result:= i; Break end; end else for i:= FromPos downto 1 do begin if IsMatchUsual(i) then begin Result:= i; Break end; end; end; function TATTextFinder.DoFindMatchRegex(FromPos: integer; var MatchPos, MatchLen: integer): boolean; var Obj: TRegExpr; begin Result:= false; if StrText='' then exit; if StrFind='' then exit; Obj:= TRegExpr.Create; try Obj.ModifierS:= false; //don't catch all text by .* Obj.ModifierM:= true; //allow to work with ^$ Obj.ModifierI:= not OptCase; try Obj.Expression:= StrFind; Obj.InputString:= StrText; Result:= Obj.ExecPos(FromPos); except if Assigned(FOnBadRegex) then FOnBadRegex(Self); Result:= false; end; if Result then begin MatchPos:= Obj.MatchPos[0]; MatchLen:= Obj.MatchLen[0]; if StrReplace<>'' then FStrReplacement:= Obj.Replace(Obj.Match[0], SRegexReplaceEscapedTabs(StrReplace), true); end; finally FreeAndNil(Obj); end; end; function TATTextFinder.DoCountMatchesUsual(FromPos: integer; AWithEvent: boolean ): Integer; var LastPos, i: Integer; Ok: boolean; begin Result:= 0; if StrText='' then exit; if StrFind='' then exit; LastPos:= Length(StrText) - Length(StrFind) + 1; for i:= FromPos to LastPos do begin if Application.Terminated then exit; if IsMatchUsual(i) then begin Inc(Result); if AWithEvent then begin FMatchPos:= i; FMatchLen:= Length(StrFind); DoOnFound; end; if Assigned(FOnProgress) then begin Ok:= true; FOnProgress(Self, i, LastPos, Ok); if not Ok then Break; end; end; end; end; function TATTextFinder.DoCountMatchesRegex(FromPos: integer; AWithEvent: boolean ): integer; var Obj: TRegExpr; Ok: boolean; begin Result:= 0; if StrFind='' then exit; if StrText='' then exit; Obj:= TRegExpr.Create; try Obj.ModifierS:= false; Obj.ModifierM:= true; Obj.ModifierI:= not OptCase; try Obj.Expression:= StrFind; Obj.InputString:= StrText; Ok:= Obj.ExecPos(FromPos); except if Assigned(FOnBadRegex) then FOnBadRegex(Self); Result:= 0; Exit; end; if Ok then begin Inc(Result); if AWithEvent then begin FMatchPos:= Obj.MatchPos[0]; FMatchLen:= Obj.MatchLen[0]; DoOnFound; end; while Obj.ExecNext do begin if Application.Terminated then exit; Inc(Result); if AWithEvent then begin FMatchPos:= Obj.MatchPos[0]; FMatchLen:= Obj.MatchLen[0]; DoOnFound; end; if Assigned(FOnProgress) then begin Ok:= true; FOnProgress(Self, Obj.MatchPos[0], Length(StrText), Ok); if not Ok then Break; end; end; end; finally FreeAndNil(Obj); end; end; procedure TATEditorFinder.UpdateBuffer; var Lens: TList; i: integer; begin Lens:= TList.Create; try Lens.Clear; for i:= 0 to FEditor.Strings.Count-1 do Lens.Add(pointer(Length(FEditor.Strings.Lines[i]))); FBuffer.Setup(FEditor.Strings.TextString, Lens, 1); finally FreeAndNil(Lens); end; StrText:= FBuffer.FText; end; constructor TATEditorFinder.Create; begin inherited; FEditor:= nil; FBuffer:= TATStringBuffer.Create; OptFromCaret:= false; OptConfirmReplace:= false; end; destructor TATEditorFinder.Destroy; begin FEditor:= nil; FreeAndNil(FBuffer); inherited; end; function TATEditorFinder.GetOffsetOfCaret: integer; var Pnt: TPoint; begin with FEditor.Carets[0] do begin Pnt.X:= PosX; Pnt.Y:= PosY; end; Result:= FBuffer.CaretToStr(Pnt); Inc(Result); //was 0-based //find-back must goto previous match if OptBack then Dec(Result, Length(StrFind)); if Result<1 then Result:= 1; end; function TATEditorFinder.DoCountAll(AWithEvent: boolean): integer; begin UpdateBuffer; if OptRegex then Result:= DoCountMatchesRegex(1, AWithEvent) else Result:= DoCountMatchesUsual(1, AWithEvent); end; function TATEditorFinder.DoReplaceAll: integer; var Ok, Changed: boolean; begin Result:= 0; if DoFindOrReplace(false, true, true, Changed) then begin if Changed then Inc(Result); while DoFindOrReplace(true, true, true, Changed) do begin if Application.Terminated then exit; if Changed then Inc(Result); if Assigned(FOnProgress) then begin Ok:= true; FOnProgress(Self, FMatchPos, Length(StrText), Ok); if not Ok then Break; end; end; end; end; procedure TATEditorFinder.DoReplaceTextInEditor(P1, P2: TPoint); var Shift, PosAfter: TPoint; Str: UnicodeString; begin if OptRegex then Str:= StrReplacement else Str:= StrReplace; FEditor.Strings.BeginUndoGroup; FEditor.Strings.TextDeleteRange(P1.X, P1.Y, P2.X, P2.Y, Shift, PosAfter); FEditor.Strings.TextInsert(P1.X, P1.Y, Str, false, Shift, PosAfter); FEditor.Strings.EndUndoGroup; //correct caret pos //(e.g. replace "dddddd" to "--": move lefter) if not OptBack then FEditor.Carets[0].PosX:= P1.X+Length(Str); end; function TATEditorFinder.GetOffsetStartPos: integer; begin if OptFromCaret then Result:= GetOffsetOfCaret else if OptRegex then Result:= 1 else if OptBack then Result:= Length(StrText) else Result:= 1; end; procedure TATEditorFinder.DoFixCaretSelectionDirection; var Caret: TATCaretItem; X1, Y1, X2, Y2: integer; bSel: boolean; begin if FEditor.Carets.Count=0 then exit; Caret:= FEditor.Carets[0]; Caret.GetRange(X1, Y1, X2, Y2, bSel); if not bSel then exit; if OptBack then begin Caret.PosX:= X1; Caret.PosY:= Y1; Caret.EndX:= X2; Caret.EndY:= Y2; end else begin Caret.PosX:= X2; Caret.PosY:= Y2; Caret.EndX:= X1; Caret.EndY:= Y1; end; end; function TATEditorFinder.DoFindOrReplace(ANext, AReplace, AForMany: boolean; out AChanged: boolean): boolean; var NStartPos: integer; begin Result:= false; AChanged:= false; if not Assigned(FEditor) then begin Showmessage('Finder.Editor not set'); Exit end; if StrFind='' then begin Showmessage('Finder.StrFind not set'); Exit end; if FEditor.Carets.Count=0 then begin Showmessage('Editor has not caret'); Exit end; if AReplace and FEditor.ModeReadOnly then exit; if OptRegex then OptBack:= false; DoFixCaretSelectionDirection; NStartPos:= GetOffsetStartPos; Result:= DoFindOrReplace_Internal(ANext, AReplace, AForMany, AChanged, NStartPos); if not Result and OptWrapped then if (not OptBack and (NStartPos>1)) or (OptBack and (NStartPosNStartPos)); if not Result then begin FMatchPos:= -1; FMatchLen:= 0; end; end; end; end; function TATEditorFinder.DoFindOrReplace_Internal(ANext, AReplace, AForMany: boolean; out AChanged: boolean; AStartPos: integer): boolean; //function usually called 1 time in outer func, //or 1-2 times if OptWrap=true var P1, P2: TPoint; ConfirmThis, ConfirmContinue: boolean; begin AChanged:= false; Result:= FindMatch(ANext, FSkipLen, AStartPos); FSkipLen:= FMatchLen; if Result then begin P1:= FBuffer.StrToCaret(MatchPos-1); P2:= FBuffer.StrToCaret(MatchPos-1+MatchLen); FEditor.DoCaretSingle(P1.X, P1.Y); if AReplace then begin ConfirmThis:= true; ConfirmContinue:= true; if OptConfirmReplace then if Assigned(FOnConfirmReplace) then FOnConfirmReplace(Self, P1, P2, AForMany, ConfirmThis, ConfirmContinue); if not ConfirmContinue then begin Result:= false; Exit; end; if ConfirmThis then begin DoReplaceTextInEditor(P1, P2); UpdateBuffer; if OptRegex then FSkipLen:= Length(StrReplacement)+GetRegexSkipIncrement else FSkipLen:= Length(StrReplace); AChanged:= true; end; end; if AReplace then //don't select FEditor.DoCaretSingle(P1.X, P1.Y) else //select to right (find forward) or to left (find back) if OptBack then FEditor.DoCaretSingle(P1.X, P1.Y, P2.X, P2.Y, true) else FEditor.DoCaretSingle(P2.X, P2.Y, P1.X, P1.Y, true); end; end; function TATEditorFinder.IsSelectionStartsAtFoundMatch: boolean; var Caret: TATCaretItem; X1, Y1, X2, Y2: integer; PosOfBegin, PosOfEnd: integer; bSel: boolean; begin Result:= false; if FEditor.Carets.Count=0 then exit; Caret:= FEditor.Carets[0]; Caret.GetRange(X1, Y1, X2, Y2, bSel); if not bSel then exit; PosOfBegin:= FBuffer.CaretToStr(Point(X1, Y1))+1; PosOfEnd:= FBuffer.CaretToStr(Point(X2, Y2))+1; //allow to replace, also if selection=Strfind Result:= ((PosOfBegin=FMatchPos) and (PosOfEnd=FMatchPos+FMatchLen)) or ((StrFind<>'') and (FEditor.TextSelected=StrFind)); end; function TATEditorFinder.DoReplaceSelectedMatch: boolean; var Caret: TATCaretItem; P1, P2: TPoint; X1, Y1, X2, Y2: integer; bSel: boolean; begin Result:= false; if not IsSelectionStartsAtFoundMatch then begin //do Find-next (from caret) DoFindOrReplace(false, false, false, bSel); exit; end; Caret:= FEditor.Carets[0]; Caret.GetRange(X1, Y1, X2, Y2, bSel); if not bSel then exit; P1:= Point(X1, Y1); P2:= Point(X2, Y2); Caret.EndX:= -1; Caret.EndY:= -1; DoReplaceTextInEditor(P1, P2); UpdateBuffer; if OptRegex then FSkipLen:= Length(StrReplacement) else FSkipLen:= Length(StrReplace); Result:= true; end; constructor TATTextFinder.Create; begin StrText:= ''; FStrFind:= ''; FStrReplace:= ''; FStrReplacement:= ''; OptBack:= false; OptCase:= false; OptWords:= false; OptRegex:= false; FMatchPos:= -1; FMatchLen:= 0; end; destructor TATTextFinder.Destroy; begin inherited Destroy; end; function TATTextFinder.FindMatch(ANext: boolean; ASkipLen: integer; AStartPos: integer): boolean; var FromPos: integer; begin Result:= false; if StrText='' then Exit; if StrFind='' then Exit; //regex code if OptRegex then begin if not ANext then FromPos:= AStartPos else FromPos:= FMatchPos+ASkipLen; Result:= DoFindMatchRegex(FromPos, FMatchPos, FMatchLen); if Result then DoOnFound; Exit end; //usual code if not ANext then begin FMatchPos:= AStartPos; end else begin if FMatchPos<=0 then FMatchPos:= 1; if not OptBack then Inc(FMatchPos, ASkipLen) else Dec(FMatchPos, ASkipLen); end; FMatchPos:= DoFindMatchUsual(FMatchPos); Result:= FMatchPos>0; if Result then begin FMatchLen:= Length(StrFind); DoOnFound; end; end; procedure TATEditorFinder.DoOnFound; var P1, P2: TPoint; begin if Assigned(FOnFound) then begin P1:= FBuffer.StrToCaret(MatchPos-1); P2:= FBuffer.StrToCaret(MatchPos-1+MatchLen); FOnFound(Self, P1, P2); end; end; function TATEditorFinder.GetRegexSkipIncrement: integer; //this is to solve loop-forever if regex "$" replaced-all to eg "===" //(need to skip one more char) begin Result:= 0; if StrFind='$' then Result:= 1; end; end.