-
-
Notifications
You must be signed in to change notification settings - Fork 563
Expand file tree
/
Copy pathsearchreplace.pas
More file actions
528 lines (462 loc) · 17.4 KB
/
searchreplace.pas
File metadata and controls
528 lines (462 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
unit searchreplace;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Vcl.ExtCtrls, SynMemo, SynEditTypes, gnugettext, VirtualTrees, SynRegExpr,
SynEditRegexSearch, SynEditMiscClasses, SynEditSearch, extra_controls,
Vcl.Menus, texteditor;
type
TfrmSearchReplace = class(TExtForm)
btnCancel: TButton;
btnReplaceAll: TButton;
lblSearch: TLabel;
chkReplace: TCheckBox;
comboSearch: TComboBox;
comboReplace: TComboBox;
grpOptions: TGroupBox;
chkCaseSensitive: TCheckBox;
chkWholeWords: TCheckBox;
chkRegularExpression: TCheckBox;
chkPromptOnReplace: TCheckBox;
grpDirection: TRadioGroup;
grpOrigin: TRadioGroup;
grpScope: TRadioGroup;
btnOK: TButton;
lblReplaceHint: TLabel;
SynEditSearch1: TSynEditSearch;
SynEditRegexSearch1: TSynEditRegexSearch;
lblSearchIn: TLabel;
comboSearchIn: TComboBox;
btnSearchHints: TButton;
btnReplaceHints: TButton;
popupSearchHints: TPopupMenu;
popupReplaceHints: TPopupMenu;
procedure ValidateControls(Sender: TObject);
procedure chkReplaceClick(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure comboSearchReplaceExit(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure DoSearchReplace(Sender: TObject);
procedure btnWithDropDownClick(Sender: TObject);
procedure menuHintClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
procedure DoSearchReplaceText;
procedure DoSearchReplaceData;
function GetEditor: TSynMemo;
function GetGrid: TVirtualStringTree;
public
{ Public declarations }
Options: TSynSearchOptions;
property Editor: TSynMemo read GetEditor;
property Grid: TVirtualStringTree read GetGrid;
end;
implementation
{$R *.dfm}
uses apphelpers, main;
procedure TfrmSearchReplace.FormCreate(Sender: TObject);
function AddItem(Menu: TPopupMenu; Code, Description, Example: String; IsRegEx: Boolean): TMenuItem;
begin
Result := TMenuItem.Create(Menu);
Result.Caption := Code + ' ' + _(Description);
Result.Hint := Example;
Result.OnClick := menuHintClick;
Result.Tag := Integer(IsRegEx);
Menu.Items.Add(Result);
end;
begin
HasSizeGrip := True;
comboSearch.Text := '';
comboReplace.Text := '';
AddItem(popupSearchHints, '^', 'Start of line', '', True);
AddItem(popupSearchHints, '$', 'End of line', '', True);
AddItem(popupSearchHints, '.', 'Any character', '', True);
AddItem(popupSearchHints, '\w', 'Any word char', 'a-z A-Z 0-9 and _', True);
AddItem(popupSearchHints, '\W', 'Any non-word char', '', True);
AddItem(popupSearchHints, '\d', 'Digit', '0..9', True);
AddItem(popupSearchHints, '\D', 'Any char except digits', '', True);
AddItem(popupSearchHints, '\s', 'Whitespace', 'space, tab, carriage return, line feed, or form feed', True);
AddItem(popupSearchHints, '\S', 'Any char except whitespaces', '', True);
AddItem(popupReplaceHints, '\n', 'New line', '', False);
AddItem(popupReplaceHints, '\t', 'Tabulator', '', False);
AddItem(popupReplaceHints, '$0', 'Entire matched text', '', True);
AddItem(popupReplaceHints, '$1', 'Text from first captured group', '', True);
AddItem(popupReplaceHints, '$2', 'Text from second captured group', '', True);
AddItem(popupReplaceHints, '$3', 'Text from third captured group', '', True);
AddItem(popupReplaceHints, '\l', 'Lowercase following char', 'aBCD', True);
AddItem(popupReplaceHints, '\L', 'Lowercase following block', 'abcd', True);
AddItem(popupReplaceHints, '\u', 'Uppercase following char', 'Abcd', True);
AddItem(popupReplaceHints, '\U', 'Uppercase following block', 'ABCD', True);
AddItem(popupReplaceHints, '\x', 'Hex code follows', '\x85', True);
end;
procedure TfrmSearchReplace.FormShow(Sender: TObject);
var
SearchText, ItemLabel: String;
QueryMemo, AnySynMemo, UsedSynMemo: TSynMemo;
ResultGrid: TVirtualStringTree;
QueryTabOpen, IsGridTextEditor, IsEditorWritable: Boolean;
ActiveQueryTab: TQueryTab;
begin
// Populate "Search in" pulldown with grid and editor
comboSearchIn.Items.Clear;
ActiveQueryTab := MainForm.QueryTabs.ActiveTab;
QueryTabOpen := Assigned(ActiveQueryTab);
SearchText := '';
UsedSynMemo := nil;
QueryMemo := MainForm.QueryTabs.ActiveMemo;
AnySynMemo := MainForm.ActiveSynMemo(True);
if Assigned(AnySynMemo) then begin
IsEditorWritable := not AnySynMemo.ReadOnly; // Support views and procedure editors
IsGridTextEditor := GetParentForm(AnySynMemo) is TfrmTextEditor; // Support grid text editor, read-only or not
if IsEditorWritable or IsGridTextEditor then
UsedSynMemo := AnySynMemo;
end;
if not Assigned(UsedSynMemo) then begin
UsedSynMemo := QueryMemo;
end;
if Assigned(UsedSynMemo) then begin
if UsedSynMemo = QueryMemo then
ItemLabel := _('SQL editor') + ': ' + ActiveQueryTab.TabSheet.Caption
else
ItemLabel := GetParentForm(UsedSynMemo).Caption;
comboSearchIn.Items.AddObject(ItemLabel, UsedSynMemo);
if UsedSynMemo.Focused then
comboSearchIn.ItemIndex := comboSearchIn.Items.Count-1;
if UsedSynMemo.SelAvail then
SearchText := UsedSynMemo.SelText
else
SearchText := UsedSynMemo.WordAtCursor;
end;
ResultGrid := MainForm.ActiveGrid;
if Assigned(ResultGrid) then begin
if QueryTabOpen then
ItemLabel := _('Result grid')+': '+ActiveQueryTab.tabsetQuery.Tabs[ActiveQueryTab.tabsetQuery.TabIndex]
else
ItemLabel := _('Data Grid');
comboSearchIn.Items.AddObject(ItemLabel, ResultGrid);
if ResultGrid.Focused then
comboSearchIn.ItemIndex := comboSearchIn.Items.Count-1;
if Assigned(ResultGrid.FocusedNode) then
SearchText := ResultGrid.Text[ResultGrid.FocusedNode, ResultGrid.FocusedColumn];
end;
if (comboSearchIn.ItemIndex = -1) and (comboSearchIn.Items.Count > 0) then begin
comboSearchIn.ItemIndex := 0;
end;
comboSearch.Items.Text := AppSettings.ReadString(asFindDialogSearchHistory);
comboReplace.Items.Text := AppSettings.ReadString(asFindDialogReplaceHistory);
// Prefill search editor with selected text
if SearchText <> '' then
comboSearch.Text := SearchText
else if comboSearch.Items.Count > 0 then
comboSearch.Text := comboSearch.Items[0];
if comboReplace.Items.Count > 0 then
comboReplace.Text := comboReplace.Items[0];
ValidateControls(Sender);
comboSearch.SetFocus;
end;
procedure TfrmSearchReplace.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
AppSettings.WriteString(asFindDialogSearchHistory, comboSearch.Items.Text);
AppSettings.WriteString(asFindDialogReplaceHistory, comboReplace.Items.Text);
end;
function TfrmSearchReplace.GetEditor: TSynMemo;
begin
// Return selected target object as editor
Result := nil;
if (comboSearchIn.ItemIndex > -1) and (comboSearchIn.Items.Objects[comboSearchIn.ItemIndex] is TSynMemo) then
Result := comboSearchIn.Items.Objects[comboSearchIn.ItemIndex] as TSynMemo;
end;
function TfrmSearchReplace.GetGrid: TVirtualStringTree;
var
o: TObject;
begin
// Return selected target object as grid
Result := nil;
if comboSearchIn.ItemIndex > -1 then begin
o := comboSearchIn.Items.Objects[comboSearchIn.ItemIndex];
if (o <> nil) and (o is TVirtualStringTree) then
Result := o as TVirtualStringTree;
end;
end;
procedure TfrmSearchReplace.menuHintClick(Sender: TObject);
var
Item: TMenuItem;
Code: String;
Combo: TComboBox;
begin
// Search or replace hint menu item clicked
Item := Sender as TMenuItem;
Code := RegExprGetMatch('^(\S+)', StripHotkey(Item.Caption), 1);
if Item.GetParentMenu = popupSearchHints then
Combo := comboSearch
else
Combo := comboReplace;
// Do not overwrite user's text
Combo.SelLength := 0;
Combo.SelText := Code;
// Be sure to support regular expression if menu item is a regex pattern
if (Item.Tag = 1) and (not chkRegularExpression.Checked) then
chkRegularExpression.Checked := True;
end;
procedure TfrmSearchReplace.btnWithDropDownClick(Sender: TObject);
var
btn: TButton;
begin
btn := Sender as TButton;
btn.DropDownMenu.Popup(btn.ClientOrigin.X, btn.ClientOrigin.Y+btn.Height);
end;
procedure TfrmSearchReplace.chkReplaceClick(Sender: TObject);
begin
// Jump to replace editor
ValidateControls(Sender);
if comboReplace.Enabled then
ActiveControl := comboReplace;
end;
procedure TfrmSearchReplace.ValidateControls(Sender: TObject);
begin
// Enable or disable various controls
comboReplace.Enabled := chkReplace.Checked;
btnReplaceHints.Enabled := chkReplace.Checked;
chkPromptOnReplace.Enabled := chkReplace.Checked;
btnReplaceAll.Enabled := chkReplace.Checked;
lblReplaceHint.Enabled := chkReplace.Checked;
if chkReplace.Checked then
btnOK.Caption := _('Replace')
else
btnOK.Caption := _('Find');
end;
procedure TfrmSearchReplace.comboSearchReplaceExit(Sender: TObject);
var
Combo: TComboBox;
i, idx: Integer;
begin
// Store search or replace text history
Combo := Sender as TComboBox;
if Combo.Text = '' then
Exit;
idx := -1;
for i:=0 to Combo.Items.Count-1 do begin
if Combo.Items[i] = Combo.Text then begin
idx := i;
break;
end;
end;
if idx > -1 then
Combo.Items.Move(idx, 0)
else
Combo.Items.Insert(0, Combo.Text);
Combo.Text := Combo.Items[0];
end;
procedure TfrmSearchReplace.DoSearchReplace(Sender: TObject);
begin
// Set SynEditSearch options once when user hits the dialog button,
// not when "Find again" action was used
if (Sender = btnOK) or (Sender = btnReplaceAll) then begin
Options := [];
if chkReplace.Checked then Include(Options, ssoReplace);
if chkCaseSensitive.Checked then Include(Options, ssoMatchCase);
if chkWholeWords.Checked then Include(Options, ssoWholeWord);
if chkPromptOnReplace.Checked and chkPromptOnReplace.Enabled then Include(Options, ssoPrompt);
if grpDirection.ItemIndex = 1 then Include(Options, ssoBackwards);
if grpOrigin.ItemIndex = 1 then Include(Options, ssoEntireScope);
if grpScope.ItemIndex = 1 then Include(Options, ssoSelectedOnly);
if ModalResult = mrAll then Include(Options, ssoReplaceAll);
// Work around multi line bug in SynEdit
if (ssoReplaceAll in Options) and (Pos('\n', comboReplace.Text) > 0) then
Include(Options, ssoBackwards);
end;
if Assigned(Editor) then
DoSearchReplaceText
else if Assigned(Grid) then
DoSearchReplaceData
else
ErrorDialog(_('No area selected'));
end;
procedure TfrmSearchReplace.DoSearchReplaceText;
var
Occurences: Integer;
OldCaretXY: TBufferCoord;
Replacement: String;
begin
if chkRegularExpression.Checked then
Editor.SearchEngine := SynEditRegexSearch1
else
Editor.SearchEngine := SynEditSearch1;
OldCaretXY := Editor.CaretXY;
Replacement := comboReplace.Text;
Replacement := StringReplace(Replacement, '\n', CRLF, [rfReplaceAll]);
Replacement := StringReplace(Replacement, '\t', #9, [rfReplaceAll]);
Editor.BeginUpdate;
MainForm.ShowStatusMsg(_('Searching ...'));
Occurences := -1; // So we can test whether an exception happened
try
Occurences := Editor.SearchReplace(
comboSearch.Text,
Replacement,
Options
);
except
on E:Exception do begin
ErrorDialog(E.ClassName + ': ' + E.Message);
ModalResult := mrNone;
end;
end;
Editor.EndUpdate;
MainForm.ShowStatusMsg;
if Occurences > -1 then begin
if ssoReplaceAll in Options then begin
MessageDialog(f_('Text "%s" replaced %s times.', [comboSearch.Text, FormatNumber(Occurences)]), mtInformation, [mbOk]);
if Occurences = 0 then
ModalResult := mrNone;
end else begin
if (OldCaretXY.Char = Editor.CaretXY.Char) and
(OldCaretXY.Line = Editor.CaretXY.Line) then begin
MessageDialog(f_('Text "%s" not found.', [comboSearch.Text]), mtInformation, [mbOk]);
ModalResult := mrNone;
end;
end;
end;
end;
procedure TfrmSearchReplace.DoSearchReplaceData;
var
Search, Replacement, CellText: String;
Node: PVirtualNode;
Column, StartAtCol: TColumnIndex;
Match, SelectedOnly, Backwards, UseRegEx: Boolean;
MatchCount, ReplaceCount: Integer;
rx: TRegExpr;
Prompt: TModalResult;
ReplaceFlags: TReplaceFlags;
NodeSelected: Boolean;
begin
// Data grid version of DoSearchReplaceText
MainForm.ShowStatusMsg(_('Searching ...'));
Search := comboSearch.Text;
Replacement := comboReplace.Text;
Replacement := StringReplace(Replacement, '\n', CRLF, [rfReplaceAll]);
Replacement := StringReplace(Replacement, '\t', #9, [rfReplaceAll]);
SelectedOnly := ssoSelectedOnly in Options;
Backwards := ssoBackwards in Options;
if ssoReplaceAll in Options then
Prompt := mrYesToAll
else
Prompt := mrYes;
Match := False;
MatchCount := 0;
ReplaceCount := 0;
ReplaceFlags := [rfReplaceAll];
if not (ssoMatchCase in Options) then
Include(ReplaceFlags, rfIgnoreCase);
NodeSelected := True;
// Init regular expression
rx := TRegExpr.Create;
UseRegEx := (ssoWholeWord in Options) or chkRegularExpression.Checked;
if chkRegularExpression.Checked then
rx.Expression := Search
else // Still used for "whole word" search
rx.Expression := '\b'+QuoteRegExprMetaChars(Search)+'\b';
rx.ModifierI := not (ssoMatchCase in Options);
// Set start row and column with regard to "Entire scope" and "Forward/Backward" mode
if (ssoEntireScope in Options) or (not Assigned(Grid.FocusedNode)) then begin
if Backwards then
Node := GetPreviousNode(Grid, nil, SelectedOnly)
else
Node := GetNextNode(Grid, nil, SelectedOnly);
StartAtCol := InvalidColumn;
end else begin
Node := Grid.FocusedNode;
if Backwards then
StartAtCol := Grid.Header.Columns.GetPreviousVisibleColumn(Grid.FocusedColumn, True)
else
StartAtCol := Grid.Header.Columns.GetNextVisibleColumn(Grid.FocusedColumn, True);
// Advance to next row if focused column is the very last column
if StartAtCol = InvalidColumn then begin
if Backwards then
Node := GetPreviousNode(Grid, Node, SelectedOnly)
else
Node := GetNextNode(Grid, Node, SelectedOnly);
end;
end;
while Assigned(Node) do begin
MainForm.AnyGridEnsureFullRow(Grid, Node);
// Find the first column
if StartAtCol > InvalidColumn then begin
Column := StartAtCol;
StartAtCol := InvalidColumn;
end else begin
if Backwards then
Column := Grid.Header.Columns.GetLastVisibleColumn(True)
else
Column := Grid.Header.Columns.GetFirstVisibleColumn(True);
end;
while Column >= 0 do begin
CellText := Grid.Text[Node, Column];
if UseRegEx then begin
Match := rx.Exec(CellText);
end else begin
if ssoMatchCase in Options then
Match := Pos(Search, CellText) > 0
else
Match := Pos(LowerCase(Search), LowerCase(CellText)) > 0;
end;
if Match then begin
Inc(MatchCount);
// Set focus on node and column
NodeSelected := SelectNode(Grid, Node, not SelectedOnly);
if not NodeSelected then
Break;
Grid.FocusedColumn := Column;
// Replace logic
if ssoReplace in Options then begin
if ssoPrompt in Options then begin
// Ask user
Prompt := MessageDialog(f_('Replace this occurrence of "%s"?', [Search]),
StrEllipsis(CellText, 500),
mtConfirmation,
[mbYes, mbYesToAll, mbNo, mbCancel]);
case Prompt of
mrYesToAll: begin
Exclude(Options, ssoPrompt);
Include(Options, ssoReplaceAll);
end;
mrCancel: Exclude(Options, ssoReplaceAll);
end;
end;
if Prompt in [mrYes, mrYesToAll] then begin
if UseRegEx then
Grid.Text[Node, Column] := rx.Replace(CellText, Replacement, chkRegularExpression.Checked)
else
Grid.Text[Node, Column] := StringReplace(CellText, Search, Replacement, ReplaceFlags);
Inc(ReplaceCount);
end;
end;
if not (ssoReplaceAll in Options) then
Break;
end;
// Advance to next column
if Backwards then
Column := Grid.Header.Columns.GetPreviousVisibleColumn(Column, True)
else
Column := Grid.Header.Columns.GetNextVisibleColumn(Column, True);
end;
if Match and (not (ssoReplaceAll in Options)) then
Break;
if not NodeSelected then
Break;
if Backwards then
Node := GetPreviousNode(Grid, Node, SelectedOnly)
else
Node := GetNextNode(Grid, Node, SelectedOnly);
end;
if (ssoReplaceAll in Options) and (MatchCount > 0) then begin
MessageDialog(f_('Text "%s" %d times replaced.', [Search, ReplaceCount]), mtInformation, [mbOk])
end;
if MatchCount = 0 then begin
MessageDialog(f_('Text "%s" not found.', [Search]), mtInformation, [mbOk]);
ModalResult := mrNone;
end;
MainForm.ShowStatusMsg;
end;
end.