{
***************************************************************************/

***************************************************************************
*                                                                         *
*   This source is free software; you can redistribute it and/or modify   *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
*   This code is distributed in the hope that it will be useful, but      *
*   WITHOUT ANY WARRANTY; without even the implied warranty of            *
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
*   General Public License for more details.                              *
*                                                                         *
*   A copy of the GNU General Public License is available on the World    *
*   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
*   obtain it by writing to the Free Software Foundation,                 *
*   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
*                                                                         *
***************************************************************************

  Author: Mattias Gaertner

  Abstract:
    Find in files non modal window.

ToDo:
  Resourcestrings

}
unit FindInFilesWnd;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils, System.UITypes, Forms, StdCtrls, Buttons, ComCtrls,
  ExtCtrls, Controls, DividerBevel, SynEdit, EnvironmentOpts, SearchPathProcs,
  MenuIntf, IDEWindowIntf, LazIDEIntf, SrcEditorIntf, IDEDialogs, LazFileUtils,
  LazLoggerBase, ProjectIntf, MacroIntf, InputHistory, EditorOptions,
  PathEditorDlg, PackageListEditorDlg;

type

  { TLazFindInFilesWindow }

  TLazFindInFilesWindow = class(TForm)
    CaseSensitiveSpeedButton: TSpeedButton;
    AutoUpdateSpeedButton: TSpeedButton;
    DirectoriesComboBox: TComboBox;
    FileMaskComboBox: TComboBox;
    TextToFindCombobox: TComboBox;
    ShowReplaceSpeedButton: TSpeedButton;
    InEditorFilesSpeedButton: TSpeedButton;
    InProjectFilesSpeedButton1: TSpeedButton;
    PkgEditSpeedButton: TSpeedButton;
    ReplaceButton: TButton;
    WherePanel: TPanel;
    PkgComboBox: TComboBox;
    ReplaceTextComboBox: TComboBox;
    ResultsTreeView: TTreeView;
    SrcDividerBevel: TDividerBevel;
    StoreAndNewSpeedButton: TSpeedButton;
    RegularExpressionsSpeedButton: TSpeedButton;
    MultilineSpeedButton: TSpeedButton;
    SynEdit1: TSynEdit;
    WholeWordsSpeedButton: TSpeedButton;
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure PkgEditSpeedButtonClick(Sender: TObject);
    procedure ShowReplaceSpeedButtonClick(Sender: TObject);
  private
    DirectoriesPathEditBtn: TPathEditorButton;
    PkgListEditBtn: TPkgListEditorButton;
    procedure DirectoriesPathEditBtnClick(Sender: TObject);
    function DirectoriesPathEditBtnExecuted(Context: String; var NewPath: String
      ): Boolean;
    procedure PkgListEditBtnClick(Sender: TObject);
    function PkgListEditBtnExecuted(Context: String; var NewPath: String
      ): Boolean;
  protected
    function GetBaseDirectory: string; virtual;
    procedure DoFirstShow; override;
    procedure InitImageList; virtual;
    procedure LoadHistory; virtual;
    procedure SaveHistory; virtual;
  public
  end;

var
  FindInFilesWindow: TLazFindInFilesWindow;

procedure RegisterFindInFilesWnd;

implementation

{$R *.lfm}

type

  { TRegisterDummy }

  TRegisterDummy = class
    procedure mnuFindInFilesWndClicked(Sender: TObject);
  end;
var
  RegisterDummy: TRegisterDummy;

procedure RegisterFindInFilesWnd;
var
  MenuCommand: TIDEMenuCommand;
begin
  {$IFNDEF EnableFindInFilesWnd}
  exit;
  {$ENDIF}
  RegisterDummy:=TRegisterDummy.Create;
  MenuCommand:=RegisterIDEMenuCommand(itmSearchFindReplace,'FindInFilesWnd','New Find In Files');
  MenuCommand.OnClick:=@RegisterDummy.mnuFindInFilesWndClicked;
end;

{ TRegisterDummy }

procedure TRegisterDummy.mnuFindInFilesWndClicked(Sender: TObject);
begin
  if FindInFilesWindow=nil then
  begin
    IDEWindowCreators.CreateForm(FindInFilesWindow,TLazFindInFilesWindow,
       false,LazarusIDE.OwningComponent);
  end;
  IDEWindowCreators.ShowForm(FindInFilesWindow,true);
end;

{ TLazFindInFilesWindow }

procedure TLazFindInFilesWindow.FormCreate(Sender: TObject);
begin
  ShowReplaceSpeedButton.Caption:='';
  ShowReplaceSpeedButton.Hint:='Show replace';
  TextToFindCombobox.TextHint:='Find text';
  StoreAndNewSpeedButton.Hint:='Store results in search results window and start a new search';
  CaseSensitiveSpeedButton.Hint:='Case Sensitive';
  WholeWordsSpeedButton.Hint:='Whole words';
  RegularExpressionsSpeedButton.Hint:='Regular Expression';
  MultilineSpeedButton.Hint:='Multi line';
  AutoUpdateSpeedButton.Hint:='Start searching while you type';

  ReplaceTextComboBox.TextHint:='Replace text';
  ReplaceButton.Hint:='Replace all found matches';

  FileMaskComboBox.TextHint:='*.pas;*.inc;*.txt;*.lfm';
  InEditorFilesSpeedButton.Hint:='Search in editor files';
  InProjectFilesSpeedButton1.Hint:='Search in project files';

  // packages
  PkgComboBox.TextHint:='pkg1;pkg2';
  PkgComboBox.Text:='';
  PkgListEditBtn := TPkgListEditorButton.Create(Self);
  with PkgListEditBtn do
  begin
    Name := 'PkgListEditBtn';
    Caption := '...';
    AutoSize := true;
    Anchors := [akRight, akTop, akBottom];
    AnchorParallel(akTop, 0, PkgComboBox);
    AnchorParallel(akBottom, 0, PkgComboBox);
    AnchorParallel(akRight, 0, WherePanel);
    ContextCaption := 'Search in packages';
    Templates:='$(ProjectPackages)';
    AssociatedComboBox:=PkgComboBox;
    OnClick := @PkgListEditBtnClick;
    OnExecuted := @PkgListEditBtnExecuted;
    Parent := WherePanel;
    TabOrder := PkgComboBox.TabOrder+1;
  end;
  PkgComboBox.AnchorToNeighbour(akRight, 0, PkgListEditBtn);

  // directories
  DirectoriesComboBox.TextHint:='folder1;folder2';
  DirectoriesComboBox.Text:='';
  DirectoriesPathEditBtn := TPathEditorButton.Create(Self);
  with DirectoriesPathEditBtn do
  begin
    Name := 'DirectoriesPathEditBtn';
    Caption := '...';
    AutoSize := true;
    Anchors := [akRight, akTop, akBottom];
    AnchorParallel(akTop, 0, DirectoriesComboBox);
    AnchorParallel(akBottom, 0, DirectoriesComboBox);
    AnchorParallel(akRight, 0, WherePanel);
    ContextCaption := 'Search in directories';
    Templates:='$(ProjPath)'+
              ';$(LazarusDir)' +
              ';$(FPCSrcDir)';
    AssociatedComboBox:=DirectoriesComboBox;
    OnClick := @DirectoriesPathEditBtnClick;
    OnExecuted := @DirectoriesPathEditBtnExecuted;
    Parent := WherePanel;
    TabOrder := DirectoriesComboBox.TabOrder+1;
  end;
  DirectoriesComboBox.AnchorToNeighbour(akRight, 0, DirectoriesPathEditBtn);

  // preview
  SrcDividerBevel.Caption:='Preview';
  SynEdit1.Lines.Text:='No source selected';

  InitImageList;
end;

procedure TLazFindInFilesWindow.FormClose(Sender: TObject;
  var CloseAction: TCloseAction);
begin
  if CloseAction=caNone then ;
  SaveHistory;
end;

procedure TLazFindInFilesWindow.FormDestroy(Sender: TObject);
begin

end;

procedure TLazFindInFilesWindow.PkgEditSpeedButtonClick(Sender: TObject);
begin

end;

procedure TLazFindInFilesWindow.ShowReplaceSpeedButtonClick(Sender: TObject);
begin
  DisableAlign;
  try
    if ShowReplaceSpeedButton.Down then
    begin
      ReplaceTextComboBox.Visible:=true;
      ReplaceButton.Visible:=true;
    end else begin
      ReplaceTextComboBox.Visible:=false;
      ReplaceButton.Visible:=false;
    end;
  finally
    EnableAlign;
  end;
end;

procedure TLazFindInFilesWindow.DirectoriesPathEditBtnClick(Sender: TObject);
begin
  DirectoriesPathEditBtn.CurrentPathEditor.BaseDirectory := GetBaseDirectory;
end;

function TLazFindInFilesWindow.DirectoriesPathEditBtnExecuted(Context: String;
  var NewPath: String): Boolean;
var
  TrimmedPath, BaseDir, Dir: String;
  p, r: Integer;
begin
  if Context='' then ;
  TrimmedPath := TrimSearchPath(NewPath, '', true);
  BaseDir:=GetBaseDirectory;
  p:=1;
  repeat
    Dir:=GetNextDirectoryInSearchPath(TrimmedPath,p);
    if Dir='' then break;
    DebugLn(['TLazFindInFilesWindow.DirectoriesPathEditBtnExecuted Dir="',Dir,'"']);
    if not IDEMacros.SubstituteMacros(Dir) then
    begin
      r:=IDEMessageDialog('Invalid Macro','Path "'+Dir+'" contains unknown macros',
        mtError,[mbCancel,mbIgnore]);
      if r<>mrIgnore then
        exit(false);
    end else begin
      debugln(['TLazFindInFilesWindow.DirectoriesPathEditBtnExecuted macro resolved Dir="',Dir,'"']);
      Dir:=ExpandFileNameUTF8(Dir,BaseDir);
      debugln(['TLazFindInFilesWindow.DirectoriesPathEditBtnExecuted expanded Dir="',Dir,'"']);
      if not DirectoryExists(Dir,true) then
      begin
        r:=IDEMessageDialog('Path not found','Path "'+Dir+'" not found',
          mtError,[mbCancel,mbIgnore]);
        if r<>mrIgnore then
          exit(false);
      end;
    end;
  until false;

  NewPath:=TrimmedPath;
  Result:=true;
end;

procedure TLazFindInFilesWindow.PkgListEditBtnClick(Sender: TObject);
begin

end;

function TLazFindInFilesWindow.PkgListEditBtnExecuted(Context: String;
  var NewPath: String): Boolean;
begin
  if Context='' then ;
  if NewPath<>PkgComboBox.Text then ;
  Result:=true;
end;

function TLazFindInFilesWindow.GetBaseDirectory: string;
var
  Proj: TLazProject;
begin
  Proj:=LazarusIDE.ActiveProject;
  if Proj<>nil then
  begin
    Result:=Proj.Directory;
    if FilenameIsAbsolute(Result) then
      exit;
    Result:=LazarusIDE.GetTestBuildDirectory;
    if FilenameIsAbsolute(Result) then
      exit;
  end;
  Result:='$(LazarusDir)';
  if not IDEMacros.SubstituteMacros(Result) then
    Result:=LazarusIDE.GetPrimaryConfigPath;
end;

procedure TLazFindInFilesWindow.DoFirstShow;
begin
  LoadHistory;
  inherited DoFirstShow;
end;

procedure TLazFindInFilesWindow.InitImageList;
begin

end;

procedure TLazFindInFilesWindow.LoadHistory;

  procedure AssignToComboBox(AComboBox: TComboBox; Strings: TStrings);
  begin
    AComboBox.Items.Assign(Strings);
    if AComboBox.Items.Count>0 then
      AComboBox.ItemIndex := 0;
  end;

  procedure AddFileToComboBox(AComboBox: TComboBox; Filename: string);
  var
    i: Integer;
  begin
    if Filename='' then exit;
    Filename:=AppendPathDelim(TrimFilename(Filename));
    for i:=0 to AComboBox.Items.Count-1 do begin
      if CompareFilenames(Filename,AComboBox.Items[i])=0 then begin
        // move to front (but not top, top should be the last used directory)
        if i>2 then
          AComboBox.Items.Move(i,1);
        exit;
      end;
    end;
    // insert in front (but not top, top should be the last used directory)
    if AComboBox.Items.Count>0 then
      i:=1
    else
      i:=0;
    AComboBox.Items.Insert(i,Filename);
  end;

  procedure AddDir(Dir: string);
  var
    i: Integer;
  begin
    for i:=0 to DirectoriesComboBox.Items.Count-1 do
    begin
      if CompareFilenames(ChompPathDelim(Dir),ChompPathDelim(DirectoriesComboBox.Items[i]))=0 then
        exit;
    end;
    DirectoriesComboBox.Items.Add(Dir);
  end;

var
  SrcEdit: TSourceEditorInterface;
begin
  SrcEdit := SourceEditorManagerIntf.ActiveEditor;

  TextToFindComboBox.Items.Assign(InputHistories.FindHistory);
  ReplaceTextComboBox.Items.Assign(InputHistories.ReplaceHistory);

  if not EditorOpts.FindTextAtCursor then begin
    if TextToFindComboBox.Items.Count>0 then begin
      TextToFindComboBox.ItemIndex:=0;
      TextToFindComboBox.SelectAll;
    end;
  end;

  // show recent directories and directory of current file
  AssignToComboBox(DirectoriesComboBox, InputHistories.FindInFilesPathHistory);
  if (SrcEdit<>nil) and (FilenameIsAbsolute(SrcEdit.FileName)) then
    AddFileToComboBox(DirectoriesComboBox, ExtractFilePath(SrcEdit.FileName));
  AddDir('$(FPCSrcDir)');
  AddDir('$(LazarusDir)');
  if DirectoriesComboBox.Items.Count>0 then
    DirectoriesComboBox.Text:=DirectoriesComboBox.Items[0];

  // recent file masks
  FileMaskComboBox.DropDownCount:=EnvironmentOptions.DropDownCount;
  AssignToComboBox(FileMaskComboBox, InputHistories.FindInFilesMaskHistory);

  // recent packages

end;

procedure TLazFindInFilesWindow.SaveHistory;
begin
  InputHistories.AddToFindInFilesMaskHistory(FileMaskComboBox.Text);
end;

finalization
  FreeAndNil(RegisterDummy);

end.

