dexed/src/u_search.pas

796 lines
20 KiB
Plaintext

unit u_search;
{$I u_defines.inc}
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
Menus, StdCtrls, actnList, Buttons, SynEdit, SynEditSearch, SynEditTypes,
u_common, u_mru, u_widget, u_synmemo, u_interfaces, u_observer, strutils,
u_writableComponent, u_dialogs, u_sharedres, u_dsgncontrols,
SynEditTextBuffer;
type
// TSearchWidget persistents settings
TSearchOptions = class(TWritableLfmTextComponent)
private
fPrompt: boolean;
fFromCur: boolean;
fRegex: boolean;
fCaseSens:boolean;
fBackWard: boolean;
fWholeWord: boolean;
fMrSearches: TStringList;
fMrReplacements: TStringList;
procedure cleanIvnalidHistoryItems;
procedure setMrSearches(value: TStringList);
procedure setMrReplacements(value: TStringList);
protected
procedure afterLoad; override;
procedure beforeSave; override;
published
property prompt: boolean read fPrompt write fPrompt;
property fromCursor: boolean read fFromCur write fFromCur;
property regex: boolean read fRegex write fRegex;
property caseSensistive: boolean read fCaseSens write fCaseSens;
property backward: boolean read fBackWard write fBackWard;
property wholeWord: boolean read fWholeWord write fWholeWord;
property recentSearches: TStringList read fMrSearches write setMrSearches;
property recentReplacements: TStringList read fMrReplacements write setMrReplacements;
public
constructor create(aOwner: TComponent); override;
destructor destroy; override;
procedure assign(source: TPersistent); override;
procedure assignTo(target: TPersistent); override;
end;
TSearchScope = (scDoc, scSel, scProj, scOpened);
TSearchWidget = class(TDexedWidget, IDocumentObserver, IProjectObserver)
btnAllScope: TBitBtn;
btnFind: TBitBtn;
btnFindAll: TBitBtn;
btnReplace: TBitBtn;
btnReplaceAll: TBitBtn;
cbToFind: TComboBox;
cbReplaceWth: TComboBox;
chkEnableRep: TCheckBox;
chkPrompt: TCheckBox;
chkRegex: TCheckBox;
chkWWord: TCheckBox;
chkBack: TCheckBox;
chkFromCur: TCheckBox;
chkCaseSens: TCheckBox;
FlowPanel1: TFlowPanel;
grpOpts: TGroupBox;
imgList: TImageList;
Panel1: TPanel;
Panel2: TPanel;
procedure btnAllScopeClick(Sender: TObject);
procedure cbReplaceWthChange(Sender: TObject);
procedure cbReplaceWthKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
procedure cbToFindChange(Sender: TObject);
procedure cbToFindKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState
);
procedure chkEnableRepChange(Sender: TObject);
private
fDoc: TDexedMemo;
fDocSelStart: TPoint;
fDocSelStop: TPoint;
fToFind: string;
fReplaceWth: string;
fActReplaceNext: TAction;
fActFindNext: TAction;
fActReplaceAll: TAction;
fActFindAll: TAction;
fSearchMru, fReplaceMru: TMruList;
fCancelAll: boolean;
fHasSearched: boolean;
fHasRestarted: boolean;
fProj: ICommonProject;
fFindScope: TSearchScope;
function getOptions: TSynSearchOptions;
procedure actReplaceAllExecute(sender: TObject);
procedure replaceEvent(Sender: TObject; const ASearch, AReplace:
string; Line, Column: integer; var ReplaceAction: TSynReplaceAction);
procedure replaceInSelEvent(Sender: TObject; const ASearch, AReplace:
string; Line, Column: integer; var ReplaceAction: TSynReplaceAction);
procedure projNew(project: ICommonProject);
procedure projChanged(project: ICommonProject);
procedure projClosing(project: ICommonProject);
procedure projFocused(project: ICommonProject);
procedure projCompiling(project: ICommonProject);
procedure projCompiled(project: ICommonProject; success: boolean);
procedure docNew(document: TDexedMemo);
procedure docClosing(document: TDexedMemo);
procedure docFocused(document: TDexedMemo);
procedure docChanged(document: TDexedMemo);
function findAll(const filename: string; lines: TStrings;
showNoResult: boolean = true): integer;
protected
procedure updateImperative; override;
public
constructor Create(aOwner: TComponent); override;
destructor Destroy; override;
procedure actFindNextExecute(sender: TObject);
procedure actReplaceNextExecute(sender: TObject);
procedure actFindAllExecute(sender: TObject);
end;
implementation
{$R *.lfm}
const
OptsFname = 'search.txt';
FindScopeStr: array[TSearchScope] of string = ('Document', 'Selection', 'Project', 'Opened docs');
{$REGION TSearchOptions ------------------------------------------------------}
constructor TSearchOptions.create(aOwner: TComponent);
begin
inherited;
fMrReplacements := TStringList.Create;
fMrSearches := TStringList.Create;
end;
destructor TSearchOptions.destroy;
begin
fMrSearches.Free;
fMrReplacements.Free;
inherited;
end;
procedure TSearchOptions.assign(source: TPersistent);
var
widg: TSearchWidget;
begin
if source is TSearchWidget then
begin
widg := TSearchWidget(source);
fMrSearches.Assign(widg.fSearchMru);
fMrReplacements.Assign(widg.fReplaceMru);
fPrompt := widg.chkPrompt.Checked;
fBackWard := widg.chkBack.Checked;
fCaseSens := widg.chkCaseSens.Checked;
fRegex := widg.chkRegex.Checked;
fFromCur := widg.chkFromCur.Checked;
fWholeWord := widg.chkWWord.Checked;
end
else inherited;
end;
procedure TSearchOptions.assignTo(target: TPersistent);
var
widg: TSearchWidget;
begin
if target is TSearchWidget then
begin
widg := TSearchWidget(target);
widg.cbToFind.Items.Assign(fMrSearches);
widg.fSearchMru.Assign(fMrSearches);
widg.cbReplaceWth.Items.Assign(fMrReplacements);
widg.fReplaceMru.Assign(fMrReplacements);
widg.chkPrompt.Checked := fPrompt;
widg.chkBack.Checked := fBackWard;
widg.chkCaseSens.Checked:= fCaseSens;
widg.chkRegex.Checked := fRegex;
widg.chkFromCur.Checked := fFromCur;
widg.chkWWord.Checked := fWholeWord;
end
else inherited;
end;
procedure TSearchOptions.setMrSearches(value: TStringList);
begin
fMrSearches.Assign(value);
end;
procedure TSearchOptions.cleanIvnalidHistoryItems;
var
i: integer;
begin
for i := fMrReplacements.Count-1 downto 0 do
if fMrReplacements[i].length > 128 then
fMrReplacements.Delete(i);
for i := fMrSearches.Count-1 downto 0 do
if fMrSearches[i].length > 128 then
fMrSearches.Delete(i);
end;
procedure TSearchOptions.setMrReplacements(value: TStringList);
begin
fMrReplacements.Assign(value);
end;
procedure TSearchOptions.afterLoad;
begin
cleanIvnalidHistoryItems;
end;
procedure TSearchOptions.beforeSave;
begin
cleanIvnalidHistoryItems;
end;
{$ENDREGION}
{$REGION Standard Comp/Obj------------------------------------------------------}
constructor TSearchWidget.Create(aOwner: TComponent);
var
fname: string;
begin
inherited;
toolbarVisible:=false;
fActFindNext := TAction.Create(self);
fActFindNext.Caption := 'Find';
fActFindNext.OnExecute := @actFindNextExecute;
fActFindAll := TAction.Create(self);
fActFindAll.Caption := 'Find all';
fActFindAll.OnExecute := @actFindAllExecute;
fActReplaceNext := TAction.Create(self);
fActReplaceNext.Caption := 'Replace';
fActReplaceNext.OnExecute := @actReplaceNextExecute;
fActReplaceAll := TAction.Create(self);
fActReplaceAll.Caption := 'Replace all';
fActReplaceAll.OnExecute := @actReplaceAllExecute;
fSearchMru := TMruList.Create;
fReplaceMru:= TMruList.Create;
fname := getDocPath + OptsFname;
if fname.fileExists then
with TSearchOptions.create(nil) do
try
loadFromFile(fname);
assignTo(self);
finally
free;
end;
btnFind.Action := fActFindNext;
btnReplace.Action := fActReplaceNext;
btnReplaceAll.Action := fActReplaceAll;
btnFindAll.Action := fActFindAll;
case GetIconScaledSize of
iss16:
begin
AssignPng(btnAllScope, 'DOCUMENT');
AssignPng(btnFind, 'FIND');
AssignPng(btnFindAll, 'FIND');
AssignPng(btnReplace, 'TEXT_REPLACE');
AssignPng(btnReplaceAll, 'TEXT_REPLACE');
end;
iss24:
begin
AssignPng(btnAllScope, 'DOCUMENT24');
AssignPng(btnFind, 'FIND24');
AssignPng(btnFindAll, 'FIND24');
AssignPng(btnReplace, 'TEXT_REPLACE24');
AssignPng(btnReplaceAll, 'TEXT_REPLACE24');
end;
iss32:
begin
AssignPng(btnAllScope, 'DOCUMENT32');
AssignPng(btnFind, 'FIND32');
AssignPng(btnFindAll, 'FIND32');
AssignPng(btnReplace, 'TEXT_REPLACE32');
AssignPng(btnReplaceAll, 'TEXT_REPLACE32');
end;
end;
btnAllScope.Caption:= FindScopeStr[fFindScope];
updateImperative;
EntitiesConnector.addObserver(self);
end;
destructor TSearchWidget.Destroy;
begin
with TSearchOptions.create(nil) do
try
assign(self);
saveToFile(getDocPath + OptsFname);
finally
free;
end;
EntitiesConnector.removeObserver(self);
fSearchMru.Free;
fReplaceMru.Free;
inherited;
end;
{$ENDREGION}
{$REGION IContextualActions---------------------------------------------------}
function TSearchWidget.getOptions: TSynSearchOptions;
begin
result := [];
if chkRegex.Checked then
result += [ssoRegExpr, ssoRegExprMultiLine];
if chkWWord.Checked then
result += [ssoWholeWord];
if chkBack.Checked then
result += [ssoBackwards];
if chkCaseSens.Checked then
result += [ssoMatchCase];
if chkPrompt.Checked then
result += [ssoPrompt];
end;
function dlgReplaceAll: TModalResult;
const
Btns = [mbYes, mbNo, mbYesToAll, mbNoToAll];
begin
exit( MessageDlg('dexed', 'Replace this match ?', mtConfirmation, Btns, ''));
end;
procedure TSearchWidget.replaceInSelEvent(Sender: TObject; const ASearch, AReplace:
string; Line, Column: integer; var ReplaceAction: TSynReplaceAction);
var
p: TPoint;
begin
if fFindScope = scSel then
begin
p := Point(Column, Line);
ReplaceAction := raReplace;
if p < fDocSelStart then
ReplaceAction := raSkip
else if p > fDocSelStop then
begin
ReplaceAction := raCancel;
fCancelAll := true;
end;
end;
end;
procedure TSearchWidget.replaceEvent(Sender: TObject; const ASearch, AReplace:
string; Line, Column: integer; var ReplaceAction: TSynReplaceAction);
var
p: TPoint;
begin
if fFindScope = scSel then
begin
p := Point(Column, Line);
if p < fDocSelStart then
begin
ReplaceAction := raSkip;
exit;
end
else if p > fDocSelStop then
begin
ReplaceAction := raCancel;
fCancelAll := true;
exit;
end;
end;
case dlgReplaceAll of
mrYes: ReplaceAction := raReplace;
mrNo: ReplaceAction := raSkip;
mrYesToAll: ReplaceAction := raReplaceAll;
mrCancel, mrClose, mrNoToAll:
begin
ReplaceAction := raCancel;
fCancelAll := true;
end;
end;
end;
procedure TSearchWidget.actFindAllExecute(sender: TObject);
var
i: integer;
c: TSynEditStringList;
f: string;
s: integer = 0;
m: IMessagesDisplay;
h: IMultiDocHandler;
begin
if (fDoc.isNil and (fFindScope <> scProj)) or
((fProj = nil) and (fFindScope = scProj)) then
exit;
fSearchMru.Insert(0,fToFind);
cbToFind.Items.Assign(fSearchMru);
case fFindScope of
scDoc, scSel:
begin
fDocSelStart := fDoc.BlockBegin;
fDocSelStop := fDoc.BlockEnd;
findAll(fDoc.fileName, fDoc.Lines, true);
end;
scProj:
begin
c := TSynEditStringList.Create;
try
for i := 0 to fProj.sourcesCount-1 do
begin
f := fProj.sourceAbsolute(i);
c.LoadFromFile(f);
s += findAll(f, c, false);
end;
if s = 0 then
begin
m := getMessageDisplay;
m.message(format('0 result for the pattern <%s>', [fToFind]),
nil, amcMisc, amkInf);
end;
finally
c.Free;
end;
end;
scOpened:
begin
c := TSynEditStringList.Create;
h := getMultiDocHandler;
try
for i := 0 to h.documentCount-1 do
begin
f := h.getDocument(i).fileName;
s += findAll(f, h.getDocument(i).Lines, false);
end;
if s = 0 then
begin
m := getMessageDisplay;
m.message(format('0 result for the pattern <%s>', [fToFind]),
nil, amcMisc, amkInf);
end;
finally
c.Free;
end;
end;
end;
end;
function TSearchWidget.findAll(const filename: string; lines: TStrings;
showNoResult: boolean = true): integer;
var
search: TSynEditSearch;
options: TSynSearchOptions;
start, stop: TPoint;
startf, stopf: TPoint;
msgs: IMessagesDisplay;
msg: string;
fmt: string;
i: integer;
res: array of TPoint = nil;
begin
result := 0;
search := TSynEditSearch.Create;
try
options := getOptions;
search.Sensitive := ssoMatchCase in options;
search.Whole := ssoWholeWord in options;
search.RegularExpressions:= ssoRegExpr in options;
search.RegExprMultiLine:=ssoRegExpr in options;
search.Pattern:=fToFind;
if (fFindScope = scSel) and fDoc.SelAvail then
begin
start := fDoc.BlockBegin;
stop := fDoc.BlockEnd;
end
else
begin
start := Point(1,1);
stop := Point(high(integer), lines.Count);
end;
while search.FindNextOne(lines, start, stop, startf, stopf) do
begin
setLength(res, length(res) + 1);
res[high(res)].X := startf.X;
res[high(res)].Y := startf.Y;
start := stopf;
end;
result := length(res);
msgs := getMessageDisplay;
if (not showNoResult and (result > 0)) or showNoResult then
begin
msg := format('%d result(s) for the pattern `%s` in %s',
[length(res), fToFind, filename]);
msgs.message(msg, nil, amcMisc, amkInf);
end;
fmt := fileName + '(%d,%d): "%s"';
for i := 0 to high(res) do
begin
msg := format(fmt, [res[i].Y, res[i].X, Trim(lines[res[i].Y-1])]);
msg := strutils.ReplaceStr(msg, fToFind, '`' + fToFind + '`');
msgs.message(msg, nil, amcMisc, amkInf);
end;
finally
search.free;
end;
end;
procedure TSearchWidget.actFindNextExecute(sender: TObject);
const
r: array[boolean] of string = ('beginning', 'end');
var
s: string;
begin
if fDoc.isNil then
exit;
fSearchMru.Insert(0, fToFind);
cbToFind.Items.Assign(fSearchMru);
if not chkFromCur.Checked then
begin
if chkBack.Checked then
fDoc.CaretXY := Point(high(Integer), high(Integer))
else
begin
if not fHasRestarted then
fDoc.CaretXY := Point(1,1);
fHasRestarted := true;
end;
end
else if fHasSearched then
begin
if chkBack.Checked then
fDoc.CaretX := fDoc.CaretX - 1
else
fDoc.CaretX := fDoc.CaretX + 1;
end;
if fDoc.SearchReplace(fToFind, '', getOptions) = 0 then
begin
s := format('the expression cannot be found, restart from the %s ?', [r[chkBack.Checked]]);
if dlgOkCancel(s) = mrOk then
begin
chkFromCur.Checked:=false;
actFindNextExecute(nil);
end;
end
else
begin
fHasSearched := true;
fHasRestarted := false;
chkFromCur.Checked := true;
fDoc.setFocus;
end;
updateImperative;
end;
procedure TSearchWidget.actReplaceNextExecute(sender: TObject);
begin
if fDoc.isNil then
exit;
fSearchMru.Insert(0, fToFind);
fReplaceMru.Insert(0, fReplaceWth);
cbToFind.Items.Assign(fSearchMru);
cbReplaceWth.Items.Assign(fReplaceMru);
if chkPrompt.Checked then
fDoc.OnReplaceText := @replaceEvent;
if not chkFromCur.Checked then
begin
if chkBack.Checked then
fDoc.CaretXY := Point(high(Integer), high(Integer))
else
fDoc.CaretXY := Point(0,0);
end
else if fHasSearched then
begin
if chkBack.Checked then
fDoc.CaretX := fDoc.CaretX - 1
else
fDoc.CaretX := fDoc.CaretX + fToFind.length;
end;
if fDoc.SearchReplace(fToFind, fReplaceWth, getOptions + [ssoReplace]) <> 0 then
fHasSearched := true;
fDoc.OnReplaceText := nil;
updateImperative;
end;
procedure TSearchWidget.actReplaceAllExecute(sender: TObject);
var
opts: TSynSearchOptions;
begin
if fDoc.isNil then
exit;
fDocSelStart := fDoc.BlockBegin;
fDocSelStop := fDoc.BlockEnd;
cbReplaceWth.Items.Assign(fReplaceMru);
opts := getOptions + [ssoReplace];
opts -= [ssoBackwards];
fSearchMru.Insert(0, fToFind);
fReplaceMru.Insert(0, fReplaceWth);
if chkPrompt.Checked then
fDoc.OnReplaceText := @replaceEvent
else if fFindScope = scSel then
begin
fDoc.CaretXY := fDocSelStart;
fDoc.OnReplaceText := @replaceInSelEvent;
// the event only called if ssoPrompt is included
opts += [ssoPrompt];
end;
if fFindScope <> scSel then
begin
fDoc.CaretXY := Point(1,1);
fDocSelStop := Point(high(integer), high(integer));
end;
while(true) do
begin
if fDoc.SearchReplace(fToFind, fReplaceWth, opts) = 0 then
break;
if fCancelAll then
begin
fCancelAll := false;
break;
end;
end;
fDoc.OnReplaceText := nil;
if fFindScope = scSel then
begin
fDoc.BlockBegin := fDocSelStart;
fDoc.BlockEnd := fDocSelStop;
end;
updateImperative;
end;
{$ENDREGION}
{$REGION IProjectObserver ----------------------------------------------------}
procedure TSearchWidget.projNew(project: ICommonProject);
begin
fProj := project;
updateImperative;
end;
procedure TSearchWidget.projChanged(project: ICommonProject);
begin
end;
procedure TSearchWidget.projClosing(project: ICommonProject);
begin
if fProj = project then
fProj := nil;
updateImperative;
end;
procedure TSearchWidget.projFocused(project: ICommonProject);
begin
fProj := project;
updateImperative;
end;
procedure TSearchWidget.projCompiling(project: ICommonProject);
begin
end;
procedure TSearchWidget.projCompiled(project: ICommonProject; success: boolean);
begin
end;
{$ENDREGION}
{$REGION IDocumentObserver ---------------------------------------------------}
procedure TSearchWidget.docNew(document: TDexedMemo);
begin
fDoc := document;
updateImperative;
end;
procedure TSearchWidget.docClosing(document: TDexedMemo);
begin
if fDoc = document then
fDoc := nil;
updateImperative;
end;
procedure TSearchWidget.docFocused(document: TDexedMemo);
begin
if fDoc = document then
exit;
fDoc := document;
updateImperative;
end;
procedure TSearchWidget.docChanged(document: TDexedMemo);
begin
end;
{$ENDREGION}
{$REGION Misc. -----------------------------------------------------------------}
procedure TSearchWidget.cbToFindChange(Sender: TObject);
begin
if Updating then
exit;
fToFind := cbToFind.Text;
fHasSearched := false;
updateImperative;
end;
procedure TSearchWidget.cbToFindKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
if Key <> 13 then
exit;
actFindNextExecute(nil);
end;
procedure TSearchWidget.chkEnableRepChange(Sender: TObject);
begin
if Updating then
exit;
updateImperative;
end;
procedure TSearchWidget.cbReplaceWthChange(Sender: TObject);
begin
if Updating then
exit;
fReplaceWth := cbReplaceWth.Text;
fHasSearched := false;
updateImperative;
end;
procedure TSearchWidget.cbReplaceWthKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key <> 13 then
exit;
actReplaceNextExecute(nil);
end;
procedure TSearchWidget.btnAllScopeClick(Sender: TObject);
begin
case fFindScope of
scDoc: fFindScope := scSel;
scSel: fFindScope := scProj;
scProj: fFindScope := scOpened;
scOpened: fFindScope := scDoc;
end;
btnAllScope.Caption:= FindScopeStr[fFindScope];
if fFindScope <> scDoc then
begin
case GetIconScaledSize of
iss16: AssignPng(btnAllScope, 'DOCUMENT_ALL');
iss24: AssignPng(btnAllScope, 'DOCUMENT_ALL24');
iss32: AssignPng(btnAllScope, 'DOCUMENT_ALL32');
end;
if fFindScope = scProj then
btnAllScope.Hint := 'find in all the project sources'
else
btnAllScope.Hint := 'find in all the documents currently opened';
end
else
begin
case GetIconScaledSize of
iss16: AssignPng(btnAllScope, 'DOCUMENT');
iss24: AssignPng(btnAllScope, 'DOCUMENT24');
iss32: AssignPng(btnAllScope, 'DOCUMENT32');
end;
btnAllScope.Hint := 'find in the selected source';
end;
updateImperative;
end;
procedure TSearchWidget.updateImperative;
var
canAll: boolean;
hasTxt: boolean;
begin
canAll := ((fDoc.isNotNil and (fFindScope <> scProj)) or
((fFindScope = scProj) and (fProj <> nil)));
hasTxt := fToFind.isNotEmpty and not fToFind.isBlank;
btnFind.Enabled := fDoc.isNotNil and hasTxt;
btnFindAll.Enabled := canAll and hasTxt;
btnReplace.Enabled := fDoc.isNotNil and chkEnableRep.Checked and fToFind.isNotEmpty;
btnReplaceAll.Enabled := btnReplace.Enabled;
cbReplaceWth.Enabled := fDoc.isNotNil and chkEnableRep.Checked;
cbToFind.Enabled := canAll;
end;
{$ENDREGION}
end.