Subversion Repositories fastphp

Rev

Rev 13 | Rev 23 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
13 daniel-mar 1
unit FindReplace;
2
 
22 daniel-mar 3
// FindReplace.pas
13 daniel-mar 4
// Source: http://www.tek-tips.com/viewthread.cfm?qid=160357
22 daniel-mar 5
//         18 Nov 2001 "bearsite4"
6
// Changes by Daniel Marschall, especially to make it compatible with TSynEdit
13 daniel-mar 7
 
8
interface
9
 
10
uses
11
  Windows, Messages, SysUtils, Classes, Dialogs, SynEdit;
12
 
13
type
14
  FindReplaceCommunication = ( frcAlertUser, frcAlertReplace, frcEndReached );
15
  {allows the replace functions to use the find function avoiding alot
16
   of code duplication.  frcAlertuser means that when the find function
17
   has reached the end of the text while searching for a word it will pop
18
   up a message saying the word can't be found. frcAlertReplace
19
   tells the find function not to display a message to the user saying that
20
   the word can't be found but instead to set the state to frcEndReached to
21
   let the replace function know it's reached the end of the text}
22
 
22 daniel-mar 23
  TReplaceFunc = function ( const S1, S2: string ): Integer;
24
 
13 daniel-mar 25
  TFindReplace = class(TComponent)
26
  private
27
    fEditor: TSynEdit;  {the richedit or memo component to hook it up to}
28
    fReplaceDialog: TReplaceDialog;  {the replace dialog}
29
    fFindDialog: TFindDialog;  {the find dialog}
30
 
31
    FindActionOnEnd: FindReplaceCommunication;  {the action the find function
32
     should take when it reaches the end of the text while searching for the
33
     word}
34
 
35
    function TestWholeWord( Sender: TFindDialog; TestString: string ): boolean;
36
    {returns a true or false depending on whether the user chose the whole word
37
     only option and whether or not the word is a whole word.  Actually, it can
38
     test multiple words in a single string as well.}
39
 
40
  protected
22 daniel-mar 41
    type
42
      TFindDirection = (sdDefault, sdForwards, sdBackwards);
43
 
13 daniel-mar 44
    procedure FindForwards( Sender: TFindDialog; start, finish: integer );
45
    {search through the editor in a forwards direction}
22 daniel-mar 46
    procedure FindBackwards( Sender: TFindDialog; start, finish: integer );
13 daniel-mar 47
    {search through the editor in a backwards direction}
48
 
49
    {defined event handlers}
50
    procedure OnFind( Sender: TObject ); virtual;
51
    procedure OnReplace( Sender: TObject ); virtual;
52
 
53
    {the centralised find/replace functions}
54
    function TryAndMatch( Sender: TFindDialog; index, finish: integer ): boolean; virtual;
22 daniel-mar 55
    function TryAndReplace(dialog: TReplaceDialog): boolean; virtual;
13 daniel-mar 56
 
22 daniel-mar 57
    procedure DoReplace(dialog: TReplaceDialog); virtual;
13 daniel-mar 58
    {the replace function that coordinates all the work}
22 daniel-mar 59
    procedure DoReplaceAll(dialog: TReplaceDialog); virtual;
13 daniel-mar 60
    {the replace all function that coordinates all the work}
61
 
22 daniel-mar 62
    procedure DoFind(dialog: TFindDialog; direction: TFindDirection);
63
 
13 daniel-mar 64
  public
65
    constructor Create( AOwner: TComponent); override;
66
 
22 daniel-mar 67
    property FindDialog: TFindDialog read fFindDialog;
68
    property ReplaceDialog: TReplaceDialog read fReplaceDialog;
13 daniel-mar 69
 
22 daniel-mar 70
    procedure CloseDialogs;
71
 
13 daniel-mar 72
    procedure FindExecute;
73
    {opens the find dialog}
74
    procedure ReplaceExecute;
75
    {opens the replace dialog}
76
 
22 daniel-mar 77
    procedure FindContinue;
78
    procedure FindNext;
79
    procedure FindPrev;
80
 
13 daniel-mar 81
    procedure GoToLine( LineNumber: integer );
82
    procedure GetLineNumber( Position: Integer; var LineNumber, ColumnNumber: Integer );
83
    {returns the line and column number the cursor is on in the editor}
84
 
85
  published
22 daniel-mar 86
    property Editor: TSynEdit read fEditor write fEditor;
13 daniel-mar 87
  end;
88
 
89
(*
90
procedure Register;
91
*)
92
 
93
implementation
94
 
95
(*
96
{$R findrep.dcr}
97
*)
98
 
99
constructor TFindReplace.Create( AOwner: TComponent );
100
begin
101
  inherited;
102
 
103
  {create the find dialog}
104
  fFindDialog := TFindDialog.Create( Self );
105
  {set up the event handlers}
106
  fFindDialog.OnFind := OnFind;
107
 
108
  {create the replace dialog}
109
  fReplaceDialog := TReplaceDialog.Create( Self );
110
  {set up the event handlers}
111
  fReplaceDialog.OnReplace := OnReplace;
112
  fReplaceDialog.OnFind := OnFind;
113
 
114
  {set find's default action on end of text to alert the user.
115
   If a replace function changes this it is it's responsibility
116
   to change it back}
117
  FindActionOnEnd := frcAlertUser;
118
end;
119
 
120
procedure TFindReplace.FindForwards( Sender: TFindDialog; start, finish: integer );
121
var
122
  i: integer;
123
 
124
begin
125
 
126
  {to find the word we go through the text on a character by character
127
   basis}
128
  for i := start to finish do
129
    if TryAndMatch( Sender, i, finish ) then
130
    {if we've got a match then stop}
131
      Exit;
132
 
133
end;
134
 
22 daniel-mar 135
procedure TFindReplace.FindBackwards( Sender: TFindDialog; start, finish: Integer );
13 daniel-mar 136
{since only find has a (search) up option and replace doesn't
137
 we don't have to worry about sender since only the onFind will
22 daniel-mar 138
 be calling this function}
13 daniel-mar 139
 
140
var
141
  i: integer;
142
 
143
begin
144
  {See comments for findforward}
145
 
146
  {to find the word we go through the text on a character by character
147
   basis but working backwards}
148
  for i := finish downto start do
22 daniel-mar 149
    if TryAndMatch( Sender, i, start ) then
13 daniel-mar 150
      Exit;
151
 
152
end;
153
 
154
function TFindReplace.TryAndMatch( Sender: TFindDialog; index, finish: integer ): boolean;
155
{returns true if there was a match and false otherwise}
156
var
157
  StringToTest: string;
158
 
22 daniel-mar 159
  StringComparison: TReplaceFunc;
160
  {the function to use to compare 2 strings.  Should be assigned
161
   different values according to the search criteria}
162
 
163
   FindTextLength: integer;
164
 
165
resourcestring
166
  S_CANT_BE_FOUND = '%s could not be found';
13 daniel-mar 167
begin
22 daniel-mar 168
  FindTextLength := Length( Sender.FindText );
169
 
13 daniel-mar 170
  {create a new string to test against}
171
  StringToTest := copy( fEditor.Text, index+1, FindTextLength );
172
 
22 daniel-mar 173
  {assign a case sensitive or case insensitive string
174
   comparison function to StringComparison depending
175
   on the whether or not the user chose to match case.
176
   The functions assigned are normal VCL functions.}
177
  if frMatchCase in Sender.Options then
178
    StringComparison := CompareStr
179
  else
180
    StringComparison := CompareText;
181
 
182
  if (StringComparison( StringToTest, Sender.FindText ) = 0) and
13 daniel-mar 183
     TestWholeWord( Sender, copy( fEditor.Text, index, FindTextLength+2 ) ) then
184
  {with TestWholeWord we pass the value index not index+1 so that it will also
185
   get the previous character.  We pass the value FindTextLenght+2 so it
186
   will copy the next character after the test string aswell}
187
  begin  {if all true then we've found the text}
188
    {highlight the word}
189
    fEditor.SetFocus;
190
    fEditor.SelStart := index;
191
    fEditor.SelLength := FindTextLength;
192
 
193
    {quit the function}
194
    Result := true;  {because we've found the word}
195
    Exit;
196
  end
197
  {if we've tried the last character and we can't find it then
198
   display a message saying so.}
199
  else if (index = finish) and (FindActionOnEnd = frcAlertUser) then
22 daniel-mar 200
    ShowMessageFmt(S_CANT_BE_FOUND, [Sender.FindText])
13 daniel-mar 201
  {otherwise if the replace function requested us to keep quiet
202
   about it then don't display the message to the user}
203
  else if (index = finish) and (FindActionOnEnd = frcAlertReplace) then
204
    FindActionOnEnd := frcEndReached;
205
 
206
  Result := false;  {didn't find it}
207
end;
208
 
22 daniel-mar 209
procedure TFindReplace.DoFind(dialog: TFindDialog; direction: TFindDirection);
13 daniel-mar 210
var
211
//  highlightedText: pChar;
212
  highlightedText: string;
213
 
214
begin
22 daniel-mar 215
  if direction = sdDefault then
216
  begin
217
    if frDown in dialog.Options then
218
      direction := sdForwards
219
    else
220
      direction := sdBackwards;
221
  end;
13 daniel-mar 222
 
223
  {check if there is already some highlighted text.  If there is and
224
   this text is the text to search for then it's probably been highlighted
225
   by the previous find operation.  In this case, move selStart to
226
   the position after the final character so the find operation won't find
227
   the same word again.  If the user chose to search up then move selStart
228
   to the character before the highlighted word}
229
  if fEditor.SelLength > 0 then
230
  begin
231
    (*
232
    GetMem( highlightedText, fEditor.SelLength + 1 );
233
    fEditor.GetSelTextBuf( highlightedText, fEditor.SelLength+1 );
234
    *)
235
    highlightedText := fEditor.SelText;
236
 
237
    {compare the two strings}
22 daniel-mar 238
    if StrIComp( PChar(highlightedText), pChar( dialog.FindText ) ) = 0 then
13 daniel-mar 239
    begin
22 daniel-mar 240
      if direction = sdForwards then
13 daniel-mar 241
        fEditor.selStart := fEditor.SelStart + fEditor.SelLength
22 daniel-mar 242
      else
243
        fEditor.selStart := fEditor.SelStart - 1;
13 daniel-mar 244
    end;
245
 
246
    (*
247
    FreeMem( highlightedText, fEditor.SelLength + 1 );
248
    *)
249
  end;
250
 
251
  {begin the search}
22 daniel-mar 252
  if direction = sdForwards then  {the user choose to search down}
13 daniel-mar 253
  begin
254
    {if the user has highlighted a block of text only search
255
     within that block}
256
    if fEditor.SelLength > 0 then
22 daniel-mar 257
      FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength )
13 daniel-mar 258
    {otherwise search the whole of the text}
22 daniel-mar 259
    else
260
      FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 261
  end
262
  else  {the user chose to search up}
263
  begin
264
    {if the user has highlighted a block of text only search
265
     within that block}
266
    if fEditor.SelLength > 0 then
22 daniel-mar 267
      FindBackwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength )
13 daniel-mar 268
    {otherwise search the whole of the text}
22 daniel-mar 269
    else
270
      FindBackwards( dialog, 0, fEditor.selStart );
13 daniel-mar 271
  end;
22 daniel-mar 272
end;
13 daniel-mar 273
 
22 daniel-mar 274
procedure TFindReplace.OnFind(Sender: TObject);
275
var
276
  FindDialog: TFindDialog;
277
begin
278
  FindDialog := Sender as TFindDialog;
279
  DoFind(FindDialog, sdDefault);
13 daniel-mar 280
end;
281
 
22 daniel-mar 282
procedure TFindReplace.OnReplace( Sender: TObject );
283
var
284
  ReplaceDialog: TReplaceDialog;
13 daniel-mar 285
begin
22 daniel-mar 286
  ReplaceDialog := Sender as TReplaceDialog;
13 daniel-mar 287
 
288
  {set the action on end to alert the function not the user}
289
  FindActionOnEnd := frcAlertReplace;
290
 
291
  {now replace the word}
22 daniel-mar 292
  if frReplace in ReplaceDialog.Options then
293
    DoReplace(ReplaceDialog)
294
  else
295
    DoReplaceAll(ReplaceDialog);
13 daniel-mar 296
 
297
  {reset the action on end to alert the user}
298
  FindActionOnEnd := frcAlertUser;
299
end;
300
 
22 daniel-mar 301
procedure TFindReplace.DoReplace(dialog: TReplaceDialog);
13 daniel-mar 302
begin
303
 
304
  {if the user has highlighted a block of text only replace
305
   within that block}
306
  if fEditor.SelLength > 0 then
307
  begin
22 daniel-mar 308
    FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
309
    TryAndReplace(dialog);
13 daniel-mar 310
  end
311
  {otherwise replace within the whole of the text}
312
  else
313
  begin
22 daniel-mar 314
    FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
315
    TryAndReplace(dialog);
13 daniel-mar 316
  end;
317
 
318
end;
319
 
22 daniel-mar 320
procedure TFindReplace.DoReplaceAll(dialog: TReplaceDialog);
13 daniel-mar 321
begin
322
  {see comments for DoReplace}
323
 
324
  if fEditor.SelLength > 0 then
325
  begin
22 daniel-mar 326
    FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
13 daniel-mar 327
    {keep replacing until we reach the end of the text}
328
    while FindActionOnEnd <> frcEndReached do
329
    begin
330
      {we enclose the TryAndReplace in a loop because there might be more
331
       than one occurence of the word in the line}
22 daniel-mar 332
      while TryAndReplace(dialog) do
333
        FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
13 daniel-mar 334
 
22 daniel-mar 335
      FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
13 daniel-mar 336
    end;
337
  end
338
  else
339
  begin
22 daniel-mar 340
    FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 341
    while FindActionOnEnd <> frcEndReached do
342
    begin
22 daniel-mar 343
      while TryAndReplace(dialog) do
344
        FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 345
 
22 daniel-mar 346
      FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 347
    end;
348
  end;
349
 
350
end;
351
 
22 daniel-mar 352
function TFindReplace.TryAndReplace(dialog: TReplaceDialog): boolean;
13 daniel-mar 353
{returns true if a replacement was made and false otherwise.  This is
354
 so a function can keep calling TryAndReplace until it returns false
355
 since there might be more than one occurence of the word to replace
356
 in the line}
357
 
358
var
359
  LineNumber, ColumnNumber: integer;
360
  ReplacementString: string;  {string used to replace the text}
361
 
22 daniel-mar 362
  OldSelStart: integer;  {the position of the cursor prior to the text
13 daniel-mar 363
   being replaced}
364
 
365
 
366
begin
367
  {assume no replacement was made}
368
  Result := false;
369
 
370
  {check to see if the word was found otherwise we don't add the
371
   replaceText to the editor.  That is, only delete the selected text
372
   and insert the replacement text if the end was not reached}
373
  if not (FindActionOnEnd = frcEndReached) then
374
  begin
375
    {get the line number and column number of the cursor which
376
     is needed for string manipulations later.  We should do this
377
     before the call to clear selection}
378
    GetLineNumber( fEditor.SelStart, LineNumber, ColumnNumber );
379
    {get the position of the cursor prior to the replace operation
380
     so we cab restore it later}
381
    OldSelStart := fEditor.SelStart;
382
 
383
    {delete the unwanted word}
384
    fEditor.ClearSelection;
385
 
386
    {Add the replacement text}
387
    {Since we can't directly manipulate the Lines field of the
388
     TCustomMemo component we'll extract the line, manipulate it
389
     then put it back}
390
    ReplacementString := fEditor.Lines[ LineNumber ];
391
    {truncate the newline (#$A#$D) at the end of tempstring}
392
    SetLength( ReplacementString, Length( ReplacementString )-2 );
393
    {add the replacement text into tempstring}
22 daniel-mar 394
    Insert( dialog.ReplaceText, ReplacementString, ColumnNumber+1 );
13 daniel-mar 395
    {remove the old string and add the new string into the editor}
396
    fEditor.Lines.Delete( LineNumber );
397
    fEditor.Lines.Insert( LineNumber, ReplacementString );
398
 
399
    {set the result to true since we have made a replacement}
400
    Result := true;
401
 
402
    {reposition the cursor to the character after the last chracter in
403
     the newly replacing text.  This is mainly so we can locate multiple
404
     occurences of the to-be-replaced text in the same line}
22 daniel-mar 405
    fEditor.SelStart := oldSelStart + length( dialog.ReplaceText );
13 daniel-mar 406
  end
407
end;
408
 
409
procedure TFindReplace.FindExecute;
410
begin
411
  fFindDialog.Execute;
412
end;
413
 
414
procedure TFindReplace.ReplaceExecute;
415
begin
416
  fReplaceDialog.Execute;
417
end;
418
 
419
function TFindReplace.TestWholeWord( Sender: TFindDialog; TestString: string ): boolean;
22 daniel-mar 420
var
421
  FindTextLength: integer;
13 daniel-mar 422
begin
423
  {assume it's not a whole word}
424
  Result := false;
425
 
22 daniel-mar 426
  FindTextLength := Length( Sender.FindText );
427
 
13 daniel-mar 428
  {if the user didn't choose whole words only then basically
429
   we don't care about it so return true}
430
  if not (frWholeWord in Sender.Options) then
431
  begin
432
    Result := true;
433
    Exit;
434
  end;
435
 
436
  {Test if the word is a whole word}
437
  {Basically there are 4 cases: ( _ denotes whitespace )
438
   1. _word_
439
   2. \nword_
440
   3. _word\n
441
   4.\nword\n}
442
  {case 1,  note: #9 tab, #$A newline}
443
  if (CharInSet(TestString[1], [' ', #9 ])) and (CharInSet(TestString[FindTextLength + 2], [' ', #9 ])) then
444
    Result := true
445
  {case 2}
446
  else if(TestString[1] = #$A) and (CharInSet(TestString[FindTextLength + 2], [' ', #9 ])) then
447
    Result := true
448
  {case 3,  note: #$D end of line}
449
  else if(CharInSet(TestString[1], [' ', #9 ])) and (TestString[FindTextLength + 2] = #$D) then
450
    Result := true
451
  else if (TestString[1] = #$A) and (TestString[FindTextLength + 2] = #$D) then
452
    Result := true
453
 
454
end;
455
 
22 daniel-mar 456
procedure TFindReplace.FindContinue;
457
begin
458
  if fFindDialog.FindText = '' then
459
  begin
460
    fFindDialog.Options := fFindDialog.Options + [frDown]; // Default direction: down
461
    FindExecute;
462
  end
463
  else
464
    DoFind(fFindDialog, sdDefault);
465
end;
466
 
13 daniel-mar 467
procedure TFindReplace.FindNext;
468
begin
22 daniel-mar 469
  if fFindDialog.FindText = '' then
470
  begin
471
    fFindDialog.Options := fFindDialog.Options + [frDown];
472
    FindExecute;
473
  end
474
  else
475
    DoFind(fFindDialog, sdForwards);
13 daniel-mar 476
end;
477
 
22 daniel-mar 478
procedure TFindReplace.FindPrev;
13 daniel-mar 479
begin
480
  if fFindDialog.FindText = '' then
481
  begin
22 daniel-mar 482
    fFindDialog.Options := fFindDialog.Options - [frDown];
483
    FindExecute;
484
  end
485
  else
486
    DoFind(fFindDialog, sdBackwards);
13 daniel-mar 487
end;
488
 
489
procedure TFindReplace.GetLineNumber( Position: Integer; var LineNumber, ColumnNumber: integer );
490
var
491
  i: integer;
492
 
493
begin
494
  {initialise line number to 0}
495
  LineNumber := 0;
496
 
497
  {increment line number each time we encounter a newline (#$D) in the text}
498
  for i := 1 to Position do
499
    if fEditor.Text = #$D then
500
      inc( LineNumber );
501
 
502
  {set the column number to position first}
503
  ColumnNumber := Position;
504
  {get the columnNumber by subtracting the length of each previous line}
505
  for i := 0 to (LineNumber-1) do
506
    dec( ColumnNumber, (*Length( fEditor.Lines )*) fEditor.Lines.Strings[i].Length );
507
 
508
end;
509
 
510
procedure TFindReplace.GoToLine( LineNumber: integer );
511
var
512
  currentLine: integer;
513
  i: integer;
514
 
515
begin
516
  {set the current line to 1}
517
  currentLine := 1;
518
 
519
  {go through the whole text looking for the line}
520
  for i := 1 to fEditor.GetTextLen do
521
  begin
522
    if currentLine = LineNumber then
523
    begin
524
      {goto the position corresponding to the line}
525
      fEditor.selStart := i;
526
      fEditor.SetFocus;
527
      {quit the function}
528
      Exit;
529
    end
530
    else if fEditor.Text = #$D then
531
      inc( currentLine );
532
  end;
533
 
534
end;
535
 
22 daniel-mar 536
procedure TFindReplace.CloseDialogs;
537
begin
538
  fFindDialog.CloseDialog;
539
  fReplaceDialog.CloseDialog;
540
end;
541
 
13 daniel-mar 542
(*
543
procedure Register;
544
begin
545
  RegisterComponents('Tek-tips', [TFindReplace]);
546
end;
547
*)
548
 
549
end.