Subversion Repositories fastphp

Rev

Rev 22 | Rev 24 | 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
 
83
  published
22 daniel-mar 84
    property Editor: TSynEdit read fEditor write fEditor;
13 daniel-mar 85
  end;
86
 
87
(*
88
procedure Register;
89
*)
90
 
91
implementation
92
 
93
(*
94
{$R findrep.dcr}
95
*)
96
 
97
constructor TFindReplace.Create( AOwner: TComponent );
98
begin
99
  inherited;
100
 
101
  {create the find dialog}
102
  fFindDialog := TFindDialog.Create( Self );
103
  {set up the event handlers}
104
  fFindDialog.OnFind := OnFind;
105
 
106
  {create the replace dialog}
107
  fReplaceDialog := TReplaceDialog.Create( Self );
108
  {set up the event handlers}
109
  fReplaceDialog.OnReplace := OnReplace;
110
  fReplaceDialog.OnFind := OnFind;
23 daniel-mar 111
  fReplaceDialog.Options := fReplaceDialog.Options + [frHideWholeWord]; // TODO: currently not supported (see below)
13 daniel-mar 112
 
113
  {set find's default action on end of text to alert the user.
114
   If a replace function changes this it is it's responsibility
115
   to change it back}
116
  FindActionOnEnd := frcAlertUser;
117
end;
118
 
119
procedure TFindReplace.FindForwards( Sender: TFindDialog; start, finish: integer );
120
var
121
  i: integer;
122
 
123
begin
124
 
125
  {to find the word we go through the text on a character by character
126
   basis}
127
  for i := start to finish do
128
    if TryAndMatch( Sender, i, finish ) then
129
    {if we've got a match then stop}
130
      Exit;
131
 
132
end;
133
 
22 daniel-mar 134
procedure TFindReplace.FindBackwards( Sender: TFindDialog; start, finish: Integer );
13 daniel-mar 135
{since only find has a (search) up option and replace doesn't
136
 we don't have to worry about sender since only the onFind will
22 daniel-mar 137
 be calling this function}
13 daniel-mar 138
 
139
var
140
  i: integer;
141
 
142
begin
143
  {See comments for findforward}
144
 
145
  {to find the word we go through the text on a character by character
146
   basis but working backwards}
147
  for i := finish downto start do
22 daniel-mar 148
    if TryAndMatch( Sender, i, start ) then
13 daniel-mar 149
      Exit;
150
 
151
end;
152
 
153
function TFindReplace.TryAndMatch( Sender: TFindDialog; index, finish: integer ): boolean;
154
{returns true if there was a match and false otherwise}
155
var
156
  StringToTest: string;
157
 
22 daniel-mar 158
  StringComparison: TReplaceFunc;
159
  {the function to use to compare 2 strings.  Should be assigned
160
   different values according to the search criteria}
161
 
162
   FindTextLength: integer;
163
 
164
resourcestring
165
  S_CANT_BE_FOUND = '%s could not be found';
13 daniel-mar 166
begin
22 daniel-mar 167
  FindTextLength := Length( Sender.FindText );
168
 
13 daniel-mar 169
  {create a new string to test against}
170
  StringToTest := copy( fEditor.Text, index+1, FindTextLength );
171
 
22 daniel-mar 172
  {assign a case sensitive or case insensitive string
173
   comparison function to StringComparison depending
174
   on the whether or not the user chose to match case.
175
   The functions assigned are normal VCL functions.}
176
  if frMatchCase in Sender.Options then
177
    StringComparison := CompareStr
178
  else
179
    StringComparison := CompareText;
180
 
181
  if (StringComparison( StringToTest, Sender.FindText ) = 0) and
13 daniel-mar 182
     TestWholeWord( Sender, copy( fEditor.Text, index, FindTextLength+2 ) ) then
183
  {with TestWholeWord we pass the value index not index+1 so that it will also
184
   get the previous character.  We pass the value FindTextLenght+2 so it
185
   will copy the next character after the test string aswell}
186
  begin  {if all true then we've found the text}
187
    {highlight the word}
188
    fEditor.SetFocus;
189
    fEditor.SelStart := index;
190
    fEditor.SelLength := FindTextLength;
191
 
192
    {quit the function}
193
    Result := true;  {because we've found the word}
194
    Exit;
195
  end
196
  {if we've tried the last character and we can't find it then
197
   display a message saying so.}
198
  else if (index = finish) and (FindActionOnEnd = frcAlertUser) then
22 daniel-mar 199
    ShowMessageFmt(S_CANT_BE_FOUND, [Sender.FindText])
13 daniel-mar 200
  {otherwise if the replace function requested us to keep quiet
201
   about it then don't display the message to the user}
202
  else if (index = finish) and (FindActionOnEnd = frcAlertReplace) then
203
    FindActionOnEnd := frcEndReached;
204
 
205
  Result := false;  {didn't find it}
206
end;
207
 
22 daniel-mar 208
procedure TFindReplace.DoFind(dialog: TFindDialog; direction: TFindDirection);
13 daniel-mar 209
var
210
//  highlightedText: pChar;
211
  highlightedText: string;
212
 
213
begin
22 daniel-mar 214
  if direction = sdDefault then
215
  begin
216
    if frDown in dialog.Options then
217
      direction := sdForwards
218
    else
219
      direction := sdBackwards;
220
  end;
13 daniel-mar 221
 
222
  {check if there is already some highlighted text.  If there is and
223
   this text is the text to search for then it's probably been highlighted
224
   by the previous find operation.  In this case, move selStart to
225
   the position after the final character so the find operation won't find
226
   the same word again.  If the user chose to search up then move selStart
227
   to the character before the highlighted word}
228
  if fEditor.SelLength > 0 then
229
  begin
230
    (*
231
    GetMem( highlightedText, fEditor.SelLength + 1 );
232
    fEditor.GetSelTextBuf( highlightedText, fEditor.SelLength+1 );
233
    *)
234
    highlightedText := fEditor.SelText;
235
 
236
    {compare the two strings}
22 daniel-mar 237
    if StrIComp( PChar(highlightedText), pChar( dialog.FindText ) ) = 0 then
13 daniel-mar 238
    begin
22 daniel-mar 239
      if direction = sdForwards then
13 daniel-mar 240
        fEditor.selStart := fEditor.SelStart + fEditor.SelLength
22 daniel-mar 241
      else
242
        fEditor.selStart := fEditor.SelStart - 1;
13 daniel-mar 243
    end;
244
 
245
    (*
246
    FreeMem( highlightedText, fEditor.SelLength + 1 );
247
    *)
248
  end;
249
 
250
  {begin the search}
22 daniel-mar 251
  if direction = sdForwards then  {the user choose to search down}
13 daniel-mar 252
  begin
253
    {if the user has highlighted a block of text only search
254
     within that block}
255
    if fEditor.SelLength > 0 then
22 daniel-mar 256
      FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength )
13 daniel-mar 257
    {otherwise search the whole of the text}
22 daniel-mar 258
    else
259
      FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 260
  end
261
  else  {the user chose to search up}
262
  begin
263
    {if the user has highlighted a block of text only search
264
     within that block}
265
    if fEditor.SelLength > 0 then
22 daniel-mar 266
      FindBackwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength )
13 daniel-mar 267
    {otherwise search the whole of the text}
22 daniel-mar 268
    else
269
      FindBackwards( dialog, 0, fEditor.selStart );
13 daniel-mar 270
  end;
22 daniel-mar 271
end;
13 daniel-mar 272
 
22 daniel-mar 273
procedure TFindReplace.OnFind(Sender: TObject);
274
var
275
  FindDialog: TFindDialog;
276
begin
277
  FindDialog := Sender as TFindDialog;
278
  DoFind(FindDialog, sdDefault);
13 daniel-mar 279
end;
280
 
22 daniel-mar 281
procedure TFindReplace.OnReplace( Sender: TObject );
282
var
283
  ReplaceDialog: TReplaceDialog;
13 daniel-mar 284
begin
22 daniel-mar 285
  ReplaceDialog := Sender as TReplaceDialog;
13 daniel-mar 286
 
287
  {set the action on end to alert the function not the user}
288
  FindActionOnEnd := frcAlertReplace;
289
 
23 daniel-mar 290
  // TODO: UnDo does not work
291
 
13 daniel-mar 292
  {now replace the word}
22 daniel-mar 293
  if frReplace in ReplaceDialog.Options then
294
    DoReplace(ReplaceDialog)
295
  else
296
    DoReplaceAll(ReplaceDialog);
13 daniel-mar 297
 
298
  {reset the action on end to alert the user}
299
  FindActionOnEnd := frcAlertUser;
300
end;
301
 
22 daniel-mar 302
procedure TFindReplace.DoReplace(dialog: TReplaceDialog);
13 daniel-mar 303
begin
23 daniel-mar 304
  FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
305
  TryAndReplace(dialog);
306
  FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen ); // Jump to the next occurrence
13 daniel-mar 307
end;
308
 
22 daniel-mar 309
procedure TFindReplace.DoReplaceAll(dialog: TReplaceDialog);
13 daniel-mar 310
begin
311
  {see comments for DoReplace}
312
 
23 daniel-mar 313
  fEditor.BeginUpdate;
314
  fEditor.BeginUndoBlock;
315
  try
316
    {if the user has highlighted a block of text only replace
317
     within that block}
318
    if fEditor.SelLength > 0 then
13 daniel-mar 319
    begin
23 daniel-mar 320
      // TODO: test this functionality
321
      FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
322
      {keep replacing until we reach the end of the text}
323
      while FindActionOnEnd <> frcEndReached do
324
      begin
325
        {we enclose the TryAndReplace in a loop because there might be more
326
         than one occurence of the word in the line}
327
        while TryAndReplace(dialog) do
328
          FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
329
 
22 daniel-mar 330
        FindForwards( dialog, fEditor.selStart, fEditor.selStart + fEditor.selLength );
23 daniel-mar 331
      end;
332
    end
333
    else {otherwise replace within the whole of the text}
334
    begin
335
      FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
336
      while FindActionOnEnd <> frcEndReached do
337
      begin
338
        while TryAndReplace(dialog) do
339
          FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
13 daniel-mar 340
 
22 daniel-mar 341
        FindForwards( dialog, fEditor.selStart, fEditor.GetTextLen );
23 daniel-mar 342
      end;
13 daniel-mar 343
    end;
23 daniel-mar 344
  finally
345
    fEditor.EndUpdate;
346
    fEditor.EndUndoBlock;
13 daniel-mar 347
  end;
348
end;
349
 
22 daniel-mar 350
function TFindReplace.TryAndReplace(dialog: TReplaceDialog): boolean;
13 daniel-mar 351
{returns true if a replacement was made and false otherwise.  This is
352
 so a function can keep calling TryAndReplace until it returns false
353
 since there might be more than one occurence of the word to replace
354
 in the line}
355
 
356
var
357
  LineNumber, ColumnNumber: integer;
358
 
23 daniel-mar 359
  OldSelStart: integer;  {the position of the cursor prior to the text being replaced}
13 daniel-mar 360
 
361
 
362
begin
363
  {assume no replacement was made}
364
  Result := false;
365
 
366
  {check to see if the word was found otherwise we don't add the
367
   replaceText to the editor.  That is, only delete the selected text
368
   and insert the replacement text if the end was not reached}
369
  if not (FindActionOnEnd = frcEndReached) then
370
  begin
371
    {get the line number and column number of the cursor which
372
     is needed for string manipulations later.  We should do this
373
     before the call to clear selection}
23 daniel-mar 374
 
375
    // TODO: only replace beginning at the selected section / caret, not from beginning of the line!!
376
    LineNumber := fEditor.CaretY-1;
377
    ColumnNumber := fEditor.CaretX-1;
378
 
13 daniel-mar 379
    {get the position of the cursor prior to the replace operation
380
     so we cab restore it later}
381
    OldSelStart := fEditor.SelStart;
382
 
23 daniel-mar 383
    // Note: "fEditor.ClearSelection" can be used to delete the selected text
13 daniel-mar 384
 
23 daniel-mar 385
    // TODO: only replace beginning at the selected section / caret, not from beginning of the line
386
    // TODO: support "whole word" ?
387
    if frMatchCase in dialog.Options then
388
      fEditor.Lines[LineNumber] := StringReplace(fEditor.Lines[LineNumber], dialog.FindText, dialog.ReplaceText, [])
389
    else
390
      fEditor.Lines[LineNumber] := StringReplace(fEditor.Lines[LineNumber], dialog.FindText, dialog.ReplaceText, [rfIgnoreCase]);
13 daniel-mar 391
 
392
    {set the result to true since we have made a replacement}
393
    Result := true;
394
 
395
    {reposition the cursor to the character after the last chracter in
396
     the newly replacing text.  This is mainly so we can locate multiple
397
     occurences of the to-be-replaced text in the same line}
22 daniel-mar 398
    fEditor.SelStart := oldSelStart + length( dialog.ReplaceText );
13 daniel-mar 399
  end
400
end;
401
 
402
procedure TFindReplace.FindExecute;
403
begin
404
  fFindDialog.Execute;
405
end;
406
 
407
procedure TFindReplace.ReplaceExecute;
408
begin
409
  fReplaceDialog.Execute;
410
end;
411
 
412
function TFindReplace.TestWholeWord( Sender: TFindDialog; TestString: string ): boolean;
22 daniel-mar 413
var
414
  FindTextLength: integer;
13 daniel-mar 415
begin
416
  {assume it's not a whole word}
417
  Result := false;
418
 
22 daniel-mar 419
  FindTextLength := Length( Sender.FindText );
420
 
13 daniel-mar 421
  {if the user didn't choose whole words only then basically
422
   we don't care about it so return true}
423
  if not (frWholeWord in Sender.Options) then
424
  begin
425
    Result := true;
426
    Exit;
427
  end;
428
 
429
  {Test if the word is a whole word}
430
  {Basically there are 4 cases: ( _ denotes whitespace )
431
   1. _word_
432
   2. \nword_
433
   3. _word\n
434
   4.\nword\n}
435
  {case 1,  note: #9 tab, #$A newline}
436
  if (CharInSet(TestString[1], [' ', #9 ])) and (CharInSet(TestString[FindTextLength + 2], [' ', #9 ])) then
437
    Result := true
438
  {case 2}
439
  else if(TestString[1] = #$A) and (CharInSet(TestString[FindTextLength + 2], [' ', #9 ])) then
440
    Result := true
441
  {case 3,  note: #$D end of line}
442
  else if(CharInSet(TestString[1], [' ', #9 ])) and (TestString[FindTextLength + 2] = #$D) then
443
    Result := true
444
  else if (TestString[1] = #$A) and (TestString[FindTextLength + 2] = #$D) then
445
    Result := true
446
 
447
end;
448
 
22 daniel-mar 449
procedure TFindReplace.FindContinue;
450
begin
451
  if fFindDialog.FindText = '' then
452
  begin
453
    fFindDialog.Options := fFindDialog.Options + [frDown]; // Default direction: down
454
    FindExecute;
455
  end
456
  else
457
    DoFind(fFindDialog, sdDefault);
458
end;
459
 
13 daniel-mar 460
procedure TFindReplace.FindNext;
461
begin
22 daniel-mar 462
  if fFindDialog.FindText = '' then
463
  begin
464
    fFindDialog.Options := fFindDialog.Options + [frDown];
465
    FindExecute;
466
  end
467
  else
468
    DoFind(fFindDialog, sdForwards);
13 daniel-mar 469
end;
470
 
22 daniel-mar 471
procedure TFindReplace.FindPrev;
13 daniel-mar 472
begin
473
  if fFindDialog.FindText = '' then
474
  begin
22 daniel-mar 475
    fFindDialog.Options := fFindDialog.Options - [frDown];
476
    FindExecute;
477
  end
478
  else
479
    DoFind(fFindDialog, sdBackwards);
13 daniel-mar 480
end;
481
 
482
procedure TFindReplace.GoToLine( LineNumber: integer );
483
var
484
  currentLine: integer;
485
  i: integer;
486
 
487
begin
488
  {set the current line to 1}
489
  currentLine := 1;
490
 
491
  {go through the whole text looking for the line}
492
  for i := 1 to fEditor.GetTextLen do
493
  begin
494
    if currentLine = LineNumber then
495
    begin
496
      {goto the position corresponding to the line}
497
      fEditor.selStart := i;
498
      fEditor.SetFocus;
499
      {quit the function}
500
      Exit;
501
    end
502
    else if fEditor.Text = #$D then
503
      inc( currentLine );
504
  end;
505
 
506
end;
507
 
22 daniel-mar 508
procedure TFindReplace.CloseDialogs;
509
begin
510
  fFindDialog.CloseDialog;
511
  fReplaceDialog.CloseDialog;
512
end;
513
 
13 daniel-mar 514
(*
515
procedure Register;
516
begin
517
  RegisterComponents('Tek-tips', [TFindReplace]);
518
end;
519
*)
520
 
521
end.