Subversion Repositories fastphp

Rev

Rev 22 | Go to most recent revision | Details | Last modification | View Log | RSS feed

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