1 {
2 /***************************************************************************
3                                 SearchFrm.pas
4                              -------------------
5 
6  ***************************************************************************/
7 
8  ***************************************************************************
9  *                                                                         *
10  *   This source is free software; you can redistribute it and/or modify   *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  *   This code is distributed in the hope that it will be useful, but      *
16  *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
18  *   General Public License for more details.                              *
19  *                                                                         *
20  *   A copy of the GNU General Public License is available on the World    *
21  *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
22  *   obtain it by writing to the Free Software Foundation,                 *
23  *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
24  *                                                                         *
25  ***************************************************************************
26 }
27 unit SearchFrm;
28 
29 {$mode objfpc}{$H+}
30 
31 interface
32 
33 uses
34   // RTL + FCL
35   Classes, SysUtils, types, RegExpr,
36   // LCL
37   LCLIntf, Forms, Controls, ComCtrls, Dialogs, ExtCtrls, StdCtrls, Buttons,
38   // CodeTools
39   SourceLog, KeywordFuncLists, BasicCodeTools, FileProcs,
40   // LazUtils
41   FileUtil, LazFileUtils, LazFileCache, LazTracer,
42   // IDEIntf
43   IDEWindowIntf, LazIDEIntf, SrcEditorIntf, IDEDialogs, ProjectGroupIntf,
44   // ide
45   LazarusIDEStrConsts, InputHistory, IDEProcs, SearchResultView, Project;
46 
47 type
48 
49   { TSearchProgressForm }
50 
51   TSearchProgressForm = class(TForm)
52     btnCancel: TBitBtn;
53     MatchesLabel: TLABEL;
54     SearchingLabel: TLABEL;
55     SearchTextLabel: TLABEL;
56     lblMatches: TLABEL;
57     lblProgress: TLABEL;
58     lblSearchText: TLABEL;
59     Panel2: TPANEL;
60     procedure FormClose(Sender: TObject; var {%H-}CloseAction: TCloseAction);
61     procedure FormShow(Sender: TObject);
62     procedure OnAddMatch(const Filename: string; const StartPos, EndPos: TPoint;
63                          const Lines: string);
64     procedure SearchFormCREATE(Sender: TObject);
65     procedure SearchFormDESTROY(Sender: TObject);
66     procedure btnAbortCLICK(Sender: TObject);
67   private
68     fFlags: TSrcEditSearchOptions;
69     fAbortString: string;
70     fMask: string;
71     fMatches: longint;
72     fPad: string;
73     FProgress: TIDESearchInTextProgress;
74     fPromptOnReplace: boolean;
75     fRecursive: boolean;
76     FReplaceText: string;
77     fResultsListUpdating: boolean;
78     fResultsList: TStrings;
79     fResultsWindow: TTabSheet;
80     fSearchFileList: TStringList;
81     fSearchFiles: boolean;
82     fSearchFor: String;
83     fDirectories: string;
84     fSearchOpen: boolean;
85     fSearchActive: boolean;
86     fSearchProject: boolean;
87     fSearchProjectGroup: boolean;
88     fAborting: boolean;
89     fLastUpdateProgress: DWORD;
90     fWasActive: boolean;
91     procedure DoFindInFiles(ADirectories: string);
92     procedure DoFindInSearchList;
93     procedure SetResultsList(const AValue: TStrings);
94     procedure UpdateMatches;
95     procedure UpdateProgress(FileName: string);
PadAndShortennull96     function PadAndShorten(FileName: string): string;
97     procedure SetOptions(TheOptions: TLazFindInFileSearchOptions);
GetOptionsnull98     function GetOptions: TLazFindInFileSearchOptions;
99     procedure SearchFile(const aFilename: string);
100     procedure SetFlag(Flag: TSrcEditSearchOption; AValue: boolean);
101     procedure DoSearchAndAddToSearchResults;
DoSearchnull102     function DoSearch: integer;
103   public
104     procedure DoSearchOpenFiles;
105     procedure DoSearchActiveFile;
106     procedure DoSearchDirs;
107     procedure DoSearchProject(AProject: TProject);
108     procedure DoSearchProjectGroup;
109   public
110     property SearchDirectories: string read fDirectories write fDirectories;
111     property SearchText: string read fSearchFor write fSearchFor;
112     property ReplaceText: string read FReplaceText write FReplaceText;
113     property SearchOptions: TLazFindInFileSearchOptions read GetOptions
114                                                         write SetOptions;
115     property SearchFileList: TStringList read fSearchFileList
116                                          write fSearchFileList;
117     property ResultsList: TStrings read fResultsList write SetResultsList;
118     property SearchMask: string read fMask write fMask;
119     property Pad: string read fPad write fPad;
120     property ResultsWindow: TTabSheet read fResultsWindow write fResultsWindow;
121     property PromptOnReplace: boolean read fPromptOnReplace write fPromptOnReplace;// this is asked once and can be changed when prompting
122     property Progress: TIDESearchInTextProgress read FProgress;
123   end;
124 
125 var
126   SearchProgressForm: TSearchProgressForm;
127 
SearchInTextnull128 function SearchInText(const TheFileName: string;
129   var TheText: string;// if TheFileName='' then use TheText
130   SearchFor, ReplaceText: string;
131   Flags: TSrcEditSearchOptions; var Prompt: boolean;
132   Progress: TIDESearchInTextProgress = nil
133   ): TModalResult;
TrimLinesAndAdjustPosnull134 function TrimLinesAndAdjustPos(const Lines: string; var APosition: integer): string;
SearchInLinenull135 function SearchInLine(const SearchStr: string; SrcLog: TSourceLog;
136   LineNumber: integer; WholeWords: boolean; StartInLine: integer;
137   out MatchStartInLine: integer): boolean;
138 
139 
140 implementation
141 
142 {$R *.lfm}
143 
144 const
145   WordBreakChars = [#0..#31,'.', ',', ';', ':', '"', '''', '!', '?', '[', ']',
146                '(', ')', '{', '}', '^', '-', '=', '+', '*', '/', '\', '|', ' '];
147   WhiteSpaceChars = [' ',#10,#13,#9];
148 
SearchInLinenull149 function SearchInLine(const SearchStr: string; SrcLog: TSourceLog;
150   LineNumber: integer; WholeWords: boolean; StartInLine: integer;
151   out MatchStartInLine: integer): boolean;
152 // search SearchStr in SrcLog line
153 // returns MatchStartInLine=1 for start of line
154 var
155   LineRange: TLineRange;
156   Src: String;
157   StartPos: PChar;
158   EndPos: PChar;
159   i: Integer;
160   SearchLen: Integer;
161   LineStartPos: PChar;
162   FirstChar: Char;
163   Found: Boolean;
164   CharInFront: PChar;
165   CharBehind: PChar;
166 begin
167   Result:=false;
168   if SearchStr='' then exit;
169   SrcLog.GetLineRange(LineNumber-1,LineRange);
170   Src:=SrcLog.Source;
171   SearchLen:=length(SearchStr);
172   LineStartPos:=@Src[LineRange.StartPos];
173   StartPos:=LineStartPos+StartInLine-1;
174   EndPos:=@Src[LineRange.EndPos-SearchLen+1];
175   FirstChar:=SearchStr[1];
176   while (StartPos<EndPos) do begin
177     if FirstChar=StartPos^ then begin
178       i:=1;
179       while (i<=SearchLen) and (StartPos[i-1]=SearchStr[i]) do
180         inc(i);
181       if i>SearchLen then begin
182         Found:=true;
183         MatchStartInLine:=StartPos-LineStartPos+1;
184         if WholeWords then begin
185           CharInFront:=StartPos-1;
186           CharBehind:=StartPos+SearchLen;
187           if ((MatchStartInLine=1)
188               or (CharInFront^ in WordBreakChars))
189           and ((StartPos+SearchLen=@Src[LineRange.EndPos])
190                or (CharBehind^ in WordBreakChars))
191           then begin
192             // word start and word end
193           end else begin
194             // not whole word
195             Found:=false;
196           end;
197         end;
198         if Found then begin
199           Result:=true;
200           exit;
201         end;
202       end;
203     end;
204     inc(StartPos);
205   end;
206 end;
207 
TrimLinesAndAdjustPosnull208 function TrimLinesAndAdjustPos(const Lines: string;
209   var APosition: integer): string;
210 var
211   StartPos: Integer;
212   EndPos: Integer;
213 begin
214   if Lines='' then begin
215     Result:='';
216     exit;
217   end;
218   if LineEndCount(Lines)=0 then begin
219     StartPos:=1;
220     while (StartPos<=length(Lines)) and (Lines[StartPos] in WhiteSpaceChars) do
221       inc(StartPos);
222     if (APosition>0) and (StartPos>APosition) then
223       StartPos:=APosition;
224     EndPos:=length(Lines)+1;
225     while (EndPos>=StartPos) and (Lines[EndPos-1] in WhiteSpaceChars) do
226       dec(EndPos);
227     dec(APosition,StartPos-1);
228     Result:=copy(Lines,StartPos,EndPos-StartPos);
229   end else
230     Result:=Lines;
231 end;
232 
SearchInTextnull233 function SearchInText(const TheFileName: string;
234   var TheText: string;// if TheFileName='' then use TheText
235   SearchFor, ReplaceText: string;
236   Flags: TSrcEditSearchOptions; var Prompt: boolean;
237   Progress: TIDESearchInTextProgress = nil
238   ): TModalResult;
239 var
240   OriginalFile: TSourceLog;// The original File being searched
241   CaseFile: TSourceLog;  // The working File being searched
242   FoundStartPos: TPoint; // Position of match in line. 1 based.
243   FoundEndPos: TPoint;
244   ReplaceLineOffset: integer;// number of lines added/deleted by replacement.
245   LastReplaceLine: integer;  // last changed line by replace. 1 based
246   LastReplaceColOffset: integer;// bytes added/deleted by replace in last line
247   TempSearch: string;    // Temp Storage for the search string.
248   RE: TRegExpr;
249   Lines: String;
250 
251   SrcEditValid: Boolean;// true if SrcEdit is valid
252   SrcEdit: TSourceEditorInterface;
253   PaintLockEnabled: Boolean;
254 
255   ReplacedText: PChar;
256   ReplacedTextCapacity: integer;
257   ReplacedTextLength: integer;
258   ReplacedTextOriginalPos: integer;// 1-based. e.g. 2 bytes has been replaced => ReplacedTextOriginalPos=3.
259 
260   procedure DoAbort;
261   begin
262     if Progress<>nil then
263       Progress.Abort:=true;
264     Result:=mrAbort;
265   end;
266 
267   procedure ProcessMessages;
268   begin
269     if Application<>nil then Application.ProcessMessages;
270     if (Progress<>nil) and Progress.Abort then
271       Result:=mrAbort;
272   end;
273 
FileIsOpenInSourceEditornull274   function FileIsOpenInSourceEditor: boolean;
275   begin
276     if not SrcEditValid then begin
277       if (TheFileName<>'') and (SourceEditorManagerIntf<>nil) then
278         SrcEdit:=SourceEditorManagerIntf.SourceEditorIntfWithFilename(TheFileName)
279       else
280         SrcEdit:=nil;
281       SrcEditValid:=true;
282     end;
283     Result:=SrcEdit<>nil;
284   end;
285 
286   procedure GrowNewText(NewLength: integer);
287   var
288     NewCapacity: Integer;
289   begin
290     if NewLength<=ReplacedTextCapacity then exit;
291     // grow
292     // first double
293     NewCapacity:=ReplacedTextCapacity*2;
294     if NewLength>NewCapacity then begin
295       // double is not enough, use the original size as minimum
296       if NewCapacity<1 then
297         NewCapacity:=OriginalFile.SourceLength+1000;
298       if NewLength>NewCapacity then begin
299         // still not enough -> grow to new length
300         NewCapacity:=NewLength;
301       end;
302     end;
303     ReplacedTextCapacity:=NewCapacity;
304     ReAllocMem(ReplacedText,ReplacedTextCapacity);
305   end;
306 
307   procedure EnablePaintLock;
308   begin
309     if (not PaintLockEnabled) and FileIsOpenInSourceEditor then begin
310       PaintLockEnabled:=true;
311       SrcEdit.BeginUpdate;
312     end;
313   end;
314 
315   procedure DisablePaintLock;
316   begin
317     if PaintLockEnabled then
318       SrcEdit.EndUpdate;
319     PaintLockEnabled:=false;
320   end;
321 
322   procedure EndLocks;
323   begin
324     DisablePaintLock;
325     SrcEditValid:=false;
326   end;
327 
328   procedure DoReplaceLine;
329   var
330     AReplace: String;
331     Action: TSrcEditReplaceAction;
332     OriginalTextPos: integer; // 1-based
333     GapLength: Integer;
334     NewLength: Integer;
335     SrcEditPosValid: boolean;
336     SrcEditStartPos, SrcEditEndPos: TPoint;
337     aLastLineLength: integer;
338     aLineCount: integer;
339     i: integer;
340 
341     procedure GetSrcEditPos;
342     begin
343       if not SrcEditPosValid then begin
344         SrcEditStartPos:=FoundStartPos;
345         SrcEditEndPos:=FoundEndPos;
346         // FoundStart/EndPos contain the original position
347         // add the changes due to replacement to SrcEditStart/EndPos
348         if SrcEditStartPos.Y=LastReplaceLine then
349           inc(SrcEditStartPos.X,LastReplaceColOffset);
350         if SrcEditStartPos.Y>=LastReplaceLine then
351           inc(SrcEditStartPos.Y,ReplaceLineOffset);
352         if SrcEditEndPos.Y=LastReplaceLine then
353           inc(SrcEditEndPos.X,LastReplaceColOffset);
354         if SrcEditEndPos.Y>=LastReplaceLine then
355           inc(SrcEditEndPos.Y,ReplaceLineOffset);
356         SrcEditPosValid:=true;
357       end;
358     end;
359 
360   begin
361     // create replacement
362     AReplace:=ReplaceText;
363     if sesoRegExpr in Flags then
364       AReplace:=RE.Substitute(AReplace);
365     //DebugLn(['DoReplaceLine Replace with "',AReplace,'"']);
366 
367     SrcEditPosValid:=false;
368 
369     // ask the user
370     if Prompt and (TheFileName<>'') then begin
371       // open the place in the source editor
372       EndLocks;
373 
374       // update windows
375       ProcessMessages;
376       if Result=mrAbort then exit;
377 
378       GetSrcEditPos;
379       if LazarusIDE.DoOpenFileAndJumpToPos(TheFileName,SrcEditStartPos,
380              -1,-1,-1,[ofUseCache,ofDoNotLoadResource,ofVirtualFile,ofRegularFile])
381       <>mrOk then
382       begin
383         DoAbort;
384         exit;
385       end;
386       // select found text
387       if not FileIsOpenInSourceEditor then
388         RaiseGDBException('inconsistency');
389       SrcEdit.SelectText(SrcEditStartPos.Y,SrcEditStartPos.X,
390                          SrcEditEndPos.Y,SrcEditEndPos.X);
391       SrcEdit.AskReplace(nil,SrcEdit.Selection,AReplace,
392                          SrcEditStartPos.Y,SrcEditStartPos.X,Action);
393       case Action of
394         seraSkip: exit;
395         seraReplace: ;
396         seraReplaceAll: Prompt:=false;
397       else
398         DoAbort;
399         exit;
400       end;
401     end;
402 
403     if FileIsOpenInSourceEditor then begin
404       // change text in source editor
405       EnablePaintLock;
406       GetSrcEditPos;
407       SrcEdit.SelectText(SrcEditStartPos.Y,SrcEditStartPos.X,
408                          SrcEditEndPos.Y,SrcEditEndPos.X);
409       SrcEdit.Selection:=AReplace;
410 
411       // count total replacements and adjust offsets
412       aLineCount:=LineEndCount(AReplace,aLastLineLength);
413       //debugln(['DoReplaceLine Replace="',dbgstr(AReplace),'" aLineCount=',aLineCount,' aLastLineLength=',aLastLineLength]);
414       if aLineCount>0 then begin
415         // replaced with multiple lines
416         LastReplaceColOffset:=aLastLineLength+1-FoundEndPos.X;
417       end else begin
418         if FoundStartPos.Y<>LastReplaceLine then
419           LastReplaceColOffset:=0;
420         // replaced with some words
421         if FoundStartPos.Y=FoundEndPos.Y then begin
422           // replaced some words with some words
423           inc(LastReplaceColOffset,
424                                aLastLineLength-(FoundEndPos.X-FoundStartPos.X));
425         end else begin
426           // replaced several lines with some words
427           inc(LastReplaceColOffset,FoundStartPos.X+aLastLineLength-FoundEndPos.X);
428         end;
429       end;
430       LastReplaceLine:=FoundEndPos.Y;
431 
432       Lines := '';
433       for i := SrcEditStartPos.Y to SrcEditStartPos.Y + aLineCount do
434         Lines := Lines + SrcEdit.Lines[i-1] + LineEnding;
435       Lines:=ChompOneLineEndAtEnd(Lines);
436       if (Progress<>nil)
437       and (Progress.OnAddMatch<>nil) then begin
438         Progress.OnAddMatch(TheFileName,
439           Point(FoundStartPos.x, FoundStartPos.y + ReplaceLineOffset),
440           SrcEdit.CursorTextXY,Lines);
441       end;
442 
443       inc(ReplaceLineOffset,aLineCount-(FoundEndPos.Y-FoundStartPos.Y));
444       //DebugLn(['DoReplaceLine FoundStartPos=',dbgs(FoundStartPos),' FoundEndPos=',dbgs(FoundEndPos),' aLastLineLength=',aLastLineLength,' LastReplaceLine=',LastReplaceLine,' LastReplaceColOffset=',LastReplaceColOffset,' ReplaceLineOffset=',ReplaceLineOffset]);
445     end else begin
446       // change text in memory/disk
447       OriginalFile.LineColToPosition(FoundStartPos.Y,FoundStartPos.X,
448                                      OriginalTextPos);
449       GapLength:=OriginalTextPos-ReplacedTextOriginalPos;
450       NewLength:=ReplacedTextLength+GapLength+length(AReplace);
451       GrowNewText(NewLength);
452       // copy the text between the last replacement and this replacement
453       if GapLength>0 then begin
454         System.Move(OriginalFile.Source[ReplacedTextOriginalPos],
455                     ReplacedText[ReplacedTextLength],GapLength);
456         inc(ReplacedTextLength,GapLength);
457       end;
458       // copy the replacement
459       if AReplace<>'' then begin
460         System.Move(AReplace[1],ReplacedText[ReplacedTextLength],length(AReplace));
461         inc(ReplacedTextLength,length(AReplace));
462       end;
463       // save original position behind found position
464       OriginalFile.LineColToPosition(FoundEndPos.Y,FoundEndPos.X,
465                                      ReplacedTextOriginalPos);
466 
467       Lines:=copy(OriginalFile.GetLines(FoundStartPos.Y,FoundStartPos.Y), 1, FoundStartPos.X - 1) +
468              AReplace +
469              copy(OriginalFile.GetLines(FoundEndPos.Y,FoundEndPos.Y), FoundEndPos.x, MaxInt);
470       Lines:=ChompOneLineEndAtEnd(Lines);
471       aLineCount:=LineEndCount(AReplace,aLastLineLength);
472       if aLineCount = 0 then aLastLineLength := aLastLineLength + FoundStartPos.X;
473       if (Progress<>nil)
474       and (Progress.OnAddMatch<>nil) then begin
475         Progress.OnAddMatch(TheFileName,
476           Point(FoundStartPos.x, FoundStartPos.y + ReplaceLineOffset),
477           Point(aLastLineLength, FoundStartPos.Y + aLineCount + ReplaceLineOffset),
478           Lines);
479       end;
480 
481       inc(ReplaceLineOffset,aLineCount-(FoundEndPos.Y-FoundStartPos.Y));
482     end;
483   end;
484 
485   procedure CommitChanges;
486   var
487     GapLength: Integer;
488     NewLength: Integer;
489     NewText: string;
490     CurResult: TModalResult;
491   begin
492     EndLocks;
493     if (ReplacedText<>nil) then begin
494       if SearchInText<>mrAbort then begin
495         GapLength:=OriginalFile.SourceLength+1-ReplacedTextOriginalPos;
496         NewLength:=ReplacedTextLength+GapLength;
497         GrowNewText(NewLength);
498         // copy the text between the last and this replacement
499         if GapLength>0 then begin
500           System.Move(OriginalFile.Source[ReplacedTextOriginalPos],
501                       ReplacedText[ReplacedTextLength],GapLength);
502           inc(ReplacedTextLength,GapLength);
503         end;
504         SetLength(NewText,ReplacedTextLength);
505         if NewText<>'' then
506           System.Move(ReplacedText[0],NewText[1],length(NewText));
507         if (TheFileName<>'') then begin
508           OriginalFile.Source:=NewText;
509           if (not OriginalFile.SaveToFile(TheFileName)) then begin
510             CurResult:=MessageDlg(lisCodeToolsDefsWriteError,
511                                   Format(lisErrorWritingFile, [TheFileName]),
512                                   mtError,[mbCancel,mbAbort],0);
513             if CurResult=mrAbort then DoAbort;
514           end;
515         end else begin
516           TheText:=NewText;
517         end;
518       end;
519       FreeMem(ReplacedText);
520     end;
521   end;
522 
523 var
524   Found: Boolean;
525   Src: String;
526   NewMatchStartPos: PtrInt;
527   NewMatchEndPos: PtrInt;
528 begin
529   //debugln(['SearchInText TheFileName=',TheFileName,' SearchFor=',SearchFor,'" ReplaceText=',ReplaceText,'"']);
530 
531   if (Progress<>nil) and Progress.Abort then exit(mrAbort);
532   Result:=mrOk;
533 
534   OriginalFile:=nil;
535   CaseFile:=nil;
536   RE:=nil;
537   SrcEdit:=nil;
538   SrcEditValid:=false;
539   PaintLockEnabled:=false;
540   ReplacedText:=nil;
541   ReplacedTextCapacity:=0;
542   ReplacedTextLength:=0;
543   ReplacedTextOriginalPos:=1;
544 
545   ReplaceLineOffset:=0;
546   LastReplaceLine:=0;
547   LastReplaceColOffset:=0;
548 
549   try
550     FoundEndPos:= Point(0,0);
551     TempSearch:= SearchFor;
552 
553     // load text (to save memory, do not use codetools cache system)
554     if FileIsOpenInSourceEditor then begin
555       OriginalFile:=TSourceLog.Create(SrcEdit.GetText(false));
556     end else if TheFileName<>'' then begin
557       OriginalFile:=TSourceLog.Create('');
558       OriginalFile.LoadFromFile(TheFileName);
559     end else begin
560       OriginalFile:=TSourceLog.Create(TheText);
561     end;
562     if OriginalFile.Source='' then exit;
563 
564     CaseFile:=nil;
565 
566     if sesoRegExpr in Flags then begin
567       // Setup the regular expression search engine
568       RE:=TRegExpr.Create;
569       RE.ModifierI:=not (sesoMatchCase in Flags);
570       RE.ModifierM:=true;
571       RE.ModifierS:=sesoMultiLine in Flags;
572       Src:=OriginalFile.Source;
573       if sesoWholeWord in Flags then
574         RE.Expression:='\b'+SearchFor+'\b'
575       else
576         RE.Expression:=SearchFor;
577     end else begin
578       // convert case if necessary
579       if not (sesoMatchCase in Flags) then begin
580         CaseFile:=TSourceLog.Create(UpperCaseStr(OriginalFile.Source));
581         TempSearch:=UpperCaseStr(TempSearch);
582         Src:=CaseFile.Source;
583       end else
584         Src:=OriginalFile.Source;
585     end;
586 
587     //debugln(['TheFileName=',TheFileName,' len=',OriginalFile.SourceLength,' Cnt=',OriginalFile.LineCount,' TempSearch=',TempSearch]);
588 
589     NewMatchEndPos:=1;
590     repeat
591       Found:=false;
592       if sesoRegExpr in Flags then begin
593         // search the text for regular expression
594         RE.InputString:=Src;
595         if RE.ExecPos(NewMatchEndPos) then begin
596           Found:=true;
597           NewMatchStartPos:=RE.MatchPos[0];
598           NewMatchEndPos:=NewMatchStartPos+RE.MatchLen[0];
599         end;
600       end else begin
601         // search for normal text
602         if SearchNextInText(PChar(TempSearch),length(TempSearch),
603                             PChar(Src),length(Src),
604                             NewMatchEndPos-1,NewMatchStartPos,NewMatchEndPos,
605                             sesoWholeWord in Flags,sesoMultiLine in Flags)
606         then begin
607           Found:=true;
608           inc(NewMatchStartPos);
609           inc(NewMatchEndPos);
610         end;
611       end;
612 
613       if Found then begin
614         // found => convert position, report and/or replace
615         OriginalFile.AbsoluteToLineCol(NewMatchStartPos,
616                                        FoundStartPos.Y,FoundStartPos.X);
617         OriginalFile.AbsoluteToLineCol(NewMatchEndPos,
618                                        FoundEndPos.Y,FoundEndPos.X);
619         //DebugLn(['SearchInText NewMatchStartPos=',NewMatchStartPos,' NewMatchEndPos=',NewMatchEndPos,' FoundStartPos=',dbgs(FoundStartPos),' FoundEndPos=',dbgs(FoundEndPos),' Found="',dbgstr(copy(Src,NewMatchStartPos,NewMatchEndPos-NewMatchStartPos)),'" Replace=',sesoReplace in Flags]);
620         if sesoReplace in Flags then begin
621           DoReplaceLine
622         end else begin
623           if (Progress<>nil)
624           and (Progress.OnAddMatch<>nil) then begin
625             Lines:=OriginalFile.GetLines(FoundStartPos.Y,FoundEndPos.Y);
626             Lines:=ChompOneLineEndAtEnd(Lines);
627             if (Progress<>nil)
628             and (Progress.OnAddMatch<>nil) then begin
629               Progress.OnAddMatch(TheFileName,FoundStartPos,FoundEndPos,Lines);
630             end;
631           end;
632         end;
633       end else begin
634         // not found
635         break;
636       end;
637 
638       // check abort
639       if (Result=mrAbort) then begin
640         exit;
641       end;
642 
643     until false;
644   finally
645     CommitChanges;
646     if OriginalFile=CaseFile then
647       CaseFile:=nil;
648     FreeAndNil(OriginalFile);
649     FreeAndNil(CaseFile);
650     FreeAndNil(RE);
651   end;
652 end;//SearchFile
653 
654 
655 { TSearchProgressForm }
656 
657 procedure TSearchProgressForm.btnAbortCLICK(Sender: TObject);
658 begin
659   Progress.Abort:= true;
660 end;
661 
662 procedure TSearchProgressForm.SearchFormCREATE(Sender: TObject);
MaxWidthnull663   Function MaxWidth(const Labs : array of TLabel) : integer;
664   var i,w : integer;
665   begin
666     Result:=0;
667     for i:=low(Labs) to high(Labs) do
668       begin
669           w:=Canvas.TextWidth(Labs[i].Caption);
670           if Result<w then
671              Result:=w;
672       end;
673   end;
674 
675 var NewX : integer;
676 begin
677   //Set Defaults
678   MatchesLabel.Caption:=lissMatches;
679   SearchingLabel.Caption:=lissSearching;
680   SearchTextLabel.Caption:=lissSearchText;
681   NewX:=MatchesLabel.Left+MaxWidth([MatchesLabel,SearchingLabel,SearchTextLabel])+10;
682   lblMatches.Left:=NewX;
683   lblProgress.Left:=NewX;
684   lblSearchText.Left:=NewX;
685 
686   Caption:=dlgSearchCaption;
687   btnCancel.Caption:=lisCancel;
688 
689   fProgress:=TIDESearchInTextProgress.Create;
690   FProgress.OnAddMatch:=@OnAddMatch;
691 
692   fFlags:=[];
693   fPromptOnReplace:=true;
694   fRecursive:= True;
695   Progress.Abort:= false;
696   fAbortString:= dlgSearchAbort;
697   fPad:= '...';
698   fSearchProject:= false;
699   fSearchProjectGroup:= false;
700   fSearchOpen:= false;
701   fSearchFiles:= false;
702   fWasActive:= false;
703 end;
704 
705 procedure TSearchProgressForm.OnAddMatch(const Filename: string; const StartPos,
706   EndPos: TPoint; const Lines: string);
707 var
708   MatchLen: Integer;
709   TrimmedMatch: LongInt;
710   TrimmedLines: String;
711   LastLineLen: integer;
712 begin
713   LineEndCount(Lines,LastLineLen);
714   MatchLen:=length(Lines)-(LastLineLen+1-EndPos.X)-StartPos.X+1;
715   if MatchLen<1 then MatchLen:=1;
716   //DebugLn(['TSearchForm.OnAddMatch length(Lines)=',length(Lines),' LastLineLen=',LastLineLen,' MatchLen=',MatchLen]);
717   TrimmedMatch:=StartPos.X;
718   TrimmedLines:=TrimLinesAndAdjustPos(Lines,TrimmedMatch);
719   //DebugLn(['TSearchForm.OnAddMatch StartPos=',dbgs(StartPos),' EndPos=',dbgs(EndPos),' Lines="',Lines,'" Trimmed="',TrimmedLines,'" TrimmedMatch=',TrimmedMatch]);
720   SearchResultsView.AddMatch(fResultsWindow.PageIndex,FileName,StartPos,EndPos,
721                              TrimmedLines, TrimmedMatch, MatchLen);
722   UpdateMatches;
723 end;
724 
725 procedure TSearchProgressForm.FormClose(Sender: TObject; var CloseAction:
726   TCloseAction);
727 begin
728   fWasActive:= Active;
729 end;
730 
731 procedure TSearchProgressForm.FormShow(Sender: TObject);
732 begin
733   fWasActive:= true;
734 end;
735 
736 procedure TSearchProgressForm.SearchFormDESTROY(Sender: TObject);
737 begin
738   FreeAndNil(fProgress);
739 end;
740 
741 procedure TSearchProgressForm.SetOptions(TheOptions: TLazFindInFileSearchOptions);
742 begin
743   SetFlag(sesoWholeWord,fifWholeWord in TheOptions);
744   SetFlag(sesoReplace,fifReplace in TheOptions);
745   SetFlag(sesoReplaceAll,fifReplaceAll in TheOptions);
746   SetFlag(sesoMatchCase,fifMatchCase in TheOptions);
747   SetFlag(sesoRegExpr,fifRegExpr in TheOptions);
748   SetFlag(sesoMultiLine,fifMultiLine in TheOptions);
749   fRecursive:= (fifIncludeSubDirs in TheOptions);
750   fSearchProject:= (fifSearchProject in TheOptions);
751   fSearchProjectGroup:= (fifSearchProjectGroup in TheOptions);
752   fSearchOpen:= (fifSearchOpen in TheOptions);
753   fSearchActive:= (fifSearchActive in TheOptions);
754   fSearchFiles:= (fifSearchDirectories in TheOptions);
755 end;//SetOptions
756 
TSearchProgressForm.GetOptionsnull757 function TSearchProgressForm.GetOptions: TLazFindInFileSearchOptions;
758 begin
759   Result:=[];
760   if sesoWholeWord in fFlags then include(Result,fifWholeWord);
761   if sesoMatchCase in fFlags then include(Result,fifMatchCase);
762   if sesoReplace in fFlags then include(Result,fifReplace);
763   if sesoReplaceAll in fFlags then include(Result,fifReplaceAll);
764   if sesoRegExpr in fFlags then include(Result,fifRegExpr);
765   if sesoMultiLine in fFlags then include(Result,fifMultiLine);
766   if fRecursive then include(Result,fifIncludeSubDirs);
767   if fSearchProject then include(Result, fifSearchProject);
768   if fSearchProjectGroup then include(Result, fifSearchProjectGroup);
769   if fSearchOpen then include(Result,fifSearchOpen);
770   if fSearchActive then include(Result,fifSearchActive);
771   if fSearchFiles then include(Result,fifSearchDirectories);
772 end;//GetOptions
773 
DoSearchnull774 function TSearchProgressForm.DoSearch: integer;
775 // Search in all files and then return the number of found items.
776 begin
777   Result:= 0;
778   PromptOnReplace:=true;
779   fAborting:=false;
780   Progress.Abort:=false;
781   lblSearchText.Caption:= fSearchFor;
782   fMatches:= 0;
783   if Assigned(fResultsList) then
784   begin
785     if not fResultsListUpdating then begin
786       fResultsList.BeginUpdate;
787       fResultsListUpdating:=true;
788     end;
789     try
790       if fSearchFiles or fSearchProjectGroup then
791         DoFindInFiles(fDirectories);
792       if fSearchProject or fSearchOpen or fSearchActive then
793         DoFindInSearchList;
794       if Assigned(fResultsList) then begin
795         Result:=fResultsList.Count;     // Return the real item count.
796         if fResultsList.Count = 0 then  // Add a note to the list if no items found.
797           fResultsList.Add(Format(lisUESearchStringNotFound,[dbgstr(fSearchFor)]));
798       end;
799     finally
800       if fResultsListUpdating then begin
801         fResultsListUpdating:=false;
802         fResultsList.EndUpdate;
803       end;
804     end;
805   end;//if
806   Close;
807 end;//DoSearch
808 
809 type
810 
811   { TLazFileSearcher }
812 
813   TLazFileSearcher = class(TFileSearcher)
814   private
815     FParent: TSearchProgressForm;
816     procedure CheckAbort;
817   protected
818     procedure DoDirectoryEnter; override;
819     procedure DoDirectoryFound; override;
820     procedure DoFileFound; override;
821   public
822     constructor Create(AParent: TSearchProgressForm);
823     destructor Destroy; override;
824   end;
825 
826 { TLazFileSearcher }
827 
828 procedure TLazFileSearcher.CheckAbort;
829 begin
830   if FParent.Progress.Abort then
831   begin
832     if not FParent.FAborting then
833     begin
834       FParent.FAborting := True;
835       FParent.FResultsList.Insert(0, FParent.FAbortString);
836     end;
837 
838     Stop;
839   end;
840 end;
841 
842 procedure TLazFileSearcher.DoDirectoryEnter;
843 begin
844   CheckAbort;
845 end;
846 
847 procedure TLazFileSearcher.DoDirectoryFound;
848 begin
849   CheckAbort;
850 end;
851 
852 procedure TLazFileSearcher.DoFileFound;
853 var
854   F: String;
855 begin
856   F := FileName;
857   if FileIsTextCached(F) then
858   begin
859     FParent.UpdateProgress(F);
860     FParent.SearchFile(F);
861   end;
862   CheckAbort;
863 end;
864 
865 constructor TLazFileSearcher.Create(AParent: TSearchProgressForm);
866 begin
867   inherited Create;
868   FParent := AParent;
869 end;
870 
871 destructor TLazFileSearcher.Destroy;
872 begin
873   FParent:=nil;
874   inherited Destroy;
875 end;
876 
877 { TSearchProgressForm }
878 
879 procedure TSearchProgressForm.DoFindInFiles(ADirectories: string);
880 var
881   Searcher: TLazFileSearcher;
882   SearchPath: String;
883   p: Integer;
884   Dir: String;
885 begin
886   // if we have a list and a valid directory
887   SearchPath:='';
888   p:=1;
889   repeat
890     Dir:=GetNextDirectoryInSearchPath(ADirectories,p);
891     if Dir='' then break;
892     if DirPathExists(Dir) then
893       SearchPath:=MergeSearchPaths(SearchPath,Dir);
894   until false;
895   if SearchPath='' then
896     exit;
897   Searcher := TLazFileSearcher.Create(Self);
898   try
899     Searcher.Search(SearchPath, FMask, FRecursive);
900   finally
901     Searcher.Free;
902   end;
903 end;
904 
905 procedure TSearchProgressForm.DoFindInSearchList;
906 var
907   i: integer;
908 begin
909   if Assigned(fSearchFileList) then
910   begin
911     for i:= 0 to fSearchFileList.Count -1 do
912     begin
913       UpdateProgress(fSearchFileList[i]);
914       SearchFile(fSearchFileList[i]);
915     end;
916   end;
917 end;
918 
919 procedure TSearchProgressForm.SetResultsList(const AValue: TStrings);
920 begin
921   if fResultsList=AValue then exit;
922   if fResultsListUpdating then
923   begin
924     fResultsList.EndUpdate;
925     fResultsListUpdating:=false;
926   end;
927   fResultsList:=AValue;
928 end;
929 
930 procedure TSearchProgressForm.UpdateMatches;
931 begin
932   inc(fMatches);
933   //DebugLn(['TSearchForm.UpdateMatches ',lblMatches.Caption]);
934   lblMatches.Caption:=IntToStr(fMatches);
935 end;
936 
937 procedure TSearchProgressForm.UpdateProgress(FileName: string);
938 const
939   UpdateAfterTicks = 200; // update not more than 5 times per second
940 var
941   DisplayFileName: string;
942   ShorterFileName: String;
943   CurTick: DWORD;
944 begin
945   CurTick:=GetTickCount;
946   if Abs(int64(CurTick)-int64(fLastUpdateProgress))<UpdateAfterTicks then
947     exit;
948   fLastUpdateProgress:=CurTick;
949 
950   DisplayFileName := FileName;
951   //DebugLn(['TSearchForm.UpdateProgress DisplayFileName="',dbgstr(DisplayFileName),'"']);
952   while (lblProgress.Left + lblProgress.Canvas.TextWidth(DisplayFileName)) > lblProgress.Parent.ClientWidth-12 do
953   begin
954     ShorterFileName:= PadAndShorten(DisplayFileName);
955     if ShorterFileName=DisplayFileName then break;
956     DisplayFileName:=ShorterFileName;
957     //DebugLn(['TSearchForm.UpdateProgress Padded DisplayFileName="',dbgstr(DisplayFileName),'"']);
958   end;
959   lblProgress.Caption := DisplayFileName;
960   Application.ProcessMessages;
961 end;
962 
963 procedure TSearchProgressForm.SearchFile(const aFilename: string);
964 var
965   Src: String;
966 begin
967   fResultsList.BeginUpdate;
968   try
969     Src:='';
970     SearchInText(aFilename,Src,fSearchFor,FReplaceText,FFlags,
971                  fPromptOnReplace,Progress);
972   finally
973     fResultsList.EndUpdate;
974   end;
975 end;
976 
977 procedure TSearchProgressForm.SetFlag(Flag: TSrcEditSearchOption; AValue: boolean);
978 begin
979   if AValue then
980     Include(fFlags,Flag)
981   else
982     Exclude(fFlags,Flag);
983 end;
984 
985 procedure TSearchProgressForm.DoSearchAndAddToSearchResults;
986 var
987   ListPage: TTabSheet;
988   Cnt: integer;
989   State: TIWGetFormState;
990 begin
991   Cnt:= 0;
992   LazarusIDE.DoShowSearchResultsView(iwgfShow);
993   ListPage:=SearchResultsView.AddSearch(SearchText,SearchText,
994                             ReplaceText,SearchDirectories,SearchMask,SearchOptions);
995   try
996     (* BeginUpdate prevents ListPage from being closed,
997       other pages can still be closed or inserted, so PageIndex can change *)
998     SearchResultsView.BeginUpdate(ListPage.PageIndex);
999     ResultsList:= SearchResultsView.Items[ListPage.PageIndex];
1000     ResultsList.Clear;
1001     ResultsWindow:= ListPage;
1002     try
1003       Show; // floating window, not dockable
1004       Cnt:= DoSearch;
1005     except
1006       on E: ERegExpr do
1007         IDEMessageDialog(lisUEErrorInRegularExpression, E.Message,mtError,
1008                    [mbCancel]);
1009     end;
1010   finally
1011     ListPage.Caption:= Format('%s (%d)',[ListPage.Caption,Cnt]);
1012     SearchResultsView.EndUpdate(ListPage.PageIndex);
1013     // show, but bring to front only if Search Progress dialog was active
1014     if fWasActive then
1015       State:=iwgfShowOnTop
1016     else
1017       State:=iwgfShow;
1018     LazarusIDE.DoShowSearchResultsView(State);
1019   end;
1020 end;
1021 
1022 procedure TSearchProgressForm.DoSearchOpenFiles;
1023 var
1024   i: integer;
1025   TheFileList: TStringList;
1026   SrcEdit: TSourceEditorInterface;
1027 begin
1028   try
1029     TheFileList:= TStringList.Create;
1030     for i:= 0 to SourceEditorManagerIntf.UniqueSourceEditorCount -1 do
1031     begin
1032       //only if file exists on disk
1033       SrcEdit := SourceEditorManagerIntf.UniqueSourceEditors[i];
1034       if FilenameIsAbsolute(SrcEdit.FileName)
1035       and (not FileExistsCached(SrcEdit.FileName)) then
1036         continue;
1037       TheFileList.Add(SrcEdit.FileName);
1038     end;
1039     SearchFileList:= TheFileList;
1040     DoSearchAndAddToSearchResults;
1041   finally
1042     FreeAndNil(TheFileList);
1043   end;
1044 end;
1045 
1046 procedure TSearchProgressForm.DoSearchActiveFile;
1047 var
1048   TheFileList: TStringList;
1049 begin
1050   try
1051     TheFileList:= TStringList.Create;      // Add a single file to the list
1052     TheFileList.Add(SourceEditorManagerIntf.ActiveEditor.FileName);
1053     SearchFileList:= TheFileList;
1054     DoSearchAndAddToSearchResults;
1055   finally
1056     FreeAndNil(TheFileList);
1057   end;
1058 end;
1059 
1060 procedure TSearchProgressForm.DoSearchDirs;
1061 begin
1062   SearchFileList:= Nil;
1063   DoSearchAndAddToSearchResults;
1064 end;
1065 
1066 procedure TSearchProgressForm.DoSearchProject(AProject: TProject);
1067 var
1068   AnUnitInfo:  TUnitInfo;
1069   TheFileList: TStringList;
1070 begin
1071   try
1072     TheFileList:= TStringList.Create;
1073     AnUnitInfo:=AProject.FirstPartOfProject;
1074     while AnUnitInfo<>nil do begin
1075       //Only if file exists on disk.
1076       if FilenameIsAbsolute(AnUnitInfo.FileName)
1077       and FileExistsCached(AnUnitInfo.FileName) then
1078         TheFileList.Add(AnUnitInfo.FileName);
1079       AnUnitInfo:=AnUnitInfo.NextPartOfProject;
1080     end;
1081     SearchFileList:= TheFileList;
1082     DoSearchAndAddToSearchResults;
1083   finally
1084     FreeAndNil(TheFileList);
1085   end;
1086 end;
1087 
1088 procedure TSearchProgressForm.DoSearchProjectGroup;
1089 begin
1090   if (ProjectGroupManager=nil) or (ProjectGroupManager.CurrentProjectGroup=nil) then
1091     DoSearchProject(Project1)
1092   else begin
1093     SearchFileList:= Nil;
1094     SearchDirectories:=ProjectGroupManager.GetSrcPaths;
1095     DoSearchAndAddToSearchResults;
1096   end;
1097 end;
1098 
PadAndShortennull1099 function TSearchProgressForm.PadAndShorten(FileName: string): string;
1100 var
1101   FoundAt: integer;
1102 begin
1103   FoundAt:= System.Pos(PathDelim,FileName);
1104   if FoundAt<1 then begin
1105     Result := Filename;
1106   end else begin
1107     Result:= fPad + copy(FileName,FoundAt+1,Length(FileName));
1108   end;
1109 end;//PadAndShorten
1110 
1111 end.
1112 
1113