{
 /***************************************************************************
                       searchresultviewView.pp - SearchResult view
                       -------------------------------------------
                   TSearchResultsView is responsible for displaying the
                   Search Results of a find operation.


                   Initial Revision  : Sat Nov 8th 2003


 ***************************************************************************/

 ***************************************************************************
 *                                                                         *
 *   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 *                                                                         *
 ***************************************************************************
}
unit SearchResultView;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LCLProc, Forms, Controls, Graphics, ComCtrls, LCLType, LCLIntf, LazUTF8,
  Menus, strutils, IDEOptionDefs, LazarusIDEStrConsts, EnvironmentOpts, InputHistory,
  IDEProcs, Project, MainIntf, Clipbrd, ActnList, IDECommands, TreeFilterEdit;


type
  { TLazSearchMatchPos }
  
  TLazSearchMatchPos = class(TObject)
  private
    FFileEndPos: TPoint;
    FFilename: string;
    FFileStartPos: TPoint;
    fMatchStart: integer;
    fMatchLen: integer;
    FNextInThisLine: TLazSearchMatchPos;
    FShownFilename: string;
    FTheText: string;
  public
    property MatchStart: integer read fMatchStart write fMatchStart;// start in TheText
    property MatchLen: integer read fMatchLen write fMatchLen; // length in TheText
    property Filename: string read FFilename write FFilename;
    property FileStartPos: TPoint read FFileStartPos write FFileStartPos;
    property FileEndPos: TPoint read FFileEndPos write FFileEndPos;
    property TheText: string read FTheText write FTheText;
    property ShownFilename: string read FShownFilename write FShownFilename;
    property NextInThisLine: TLazSearchMatchPos read FNextInThisLine write FNextInThisLine;
    destructor Destroy; override;
  end;//TLazSearchMatchPos


  { TLazSearch }

  TLazSearch = Class(TObject)
  private
    FReplaceText: string;
    fSearchString: string;
    fSearchOptions: TLazFindInFileSearchOptions;
    fSearchDirectories: string;
    fSearchMask: string;
  public
    property SearchString: string read fSearchString write fSearchString;
    property ReplaceText: string read FReplaceText write FReplaceText;
    property SearchOptions: TLazFindInFileSearchOptions read fSearchOptions
                                                        write fSearchOptions;
    property SearchDirectories: string read fSearchDirectories
                                     write fSearchDirectories;
    property SearchMask: string read fSearchMask write fSearchMask;
  end;//TLazSearch


  { TLazSearchResultTV }

  TLazSearchResultTV = class(TCustomTreeView)
  private
    fSearchObject: TLazSearch;
    FSkipped: integer;
    fUpdateStrings: TStrings;
    fUpdating: boolean;
    fUpdateCount: integer;
    FSearchInListPhrases: string;
    fFiltered: Boolean;
    procedure SetSkipped(const AValue: integer);
    procedure AddNode(Line: string; MatchPos: TLazSearchMatchPos);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property SearchObject: TLazSearch read fSearchObject write fSearchObject;
    procedure BeginUpdate;
    procedure EndUpdate;
    procedure ShortenPaths;
    procedure FreeObjectsTN(tnItems: TTreeNodes);
    procedure FreeObjects(slItems: TStrings);
    function BeautifyLine(const Filename: string; X, Y: integer; const Line: string): string;
    function BeautifyLine(SearchPos: TLazSearchMatchPos): string;
    property Filtered: Boolean read fFiltered write fFiltered;
    property SearchInListPhrases: string read FSearchInListPhrases write FSearchInListPhrases;
    property UpdateItems: TStrings read fUpdateStrings write fUpdateStrings;
    property Updating: boolean read fUpdating;
    property Skipped: integer read FSkipped write SetSkipped;
    property Items;
    function ItemsAsStrings: TStrings;
  end;


  { TSearchResultsView }

  TSearchResultsView = class(TForm)
    actClosePage: TAction;
    actNextPage: TAction;
    actPrevPage: TAction;
    ActionList: TActionList;
    MenuItem1: TMenuItem;
    mniCollapseAll: TMenuItem;
    mniExpandAll: TMenuItem;
    mniCopySelected: TMenuItem;
    mniCopyAll: TMenuItem;
    mniCopyItem: TMenuItem;
    popList: TPopupMenu;
    ImageList: TImageList;
    ResultsNoteBook: TPageControl;
    ToolBar: TToolBar;
    SearchAgainButton: TToolButton;
    ToolButton3: TToolButton;
    ClosePageButton: TToolButton;
    SearchInListEdit: TTreeFilterEdit;
    procedure actNextPageExecute(Sender: TObject);
    procedure actPrevPageExecute(Sender: TObject);
    procedure ClosePageButtonClick(Sender: TObject);
    procedure Form1Create(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormKeyDown(Sender: TObject; var Key: Word; {%H-}Shift: TShiftState);
    procedure mniCopyAllClick(Sender: TObject);
    procedure mniCopyItemClick(Sender: TObject);
    procedure mniCopySelectedClick(Sender: TObject);
    procedure mniExpandAllClick(Sender: TObject);
    procedure mniCollapseAllClick(Sender: TObject);
    procedure ResultsNoteBookMouseDown(Sender: TObject; Button: TMouseButton;
      {%H-}Shift: TShiftState; X, Y: Integer);
    procedure TreeViewKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure ResultsNoteBookClosetabclicked(Sender: TObject);
    procedure SearchAgainButtonClick(Sender: TObject);
    procedure TreeViewAdvancedCustomDrawItem(Sender: TCustomTreeView;
      Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
      var {%H-}PaintImages, {%H-}DefaultDraw: Boolean);
    procedure LazTVShowHint(Sender: TObject; {%H-}HintInfo: PHintInfo);
    procedure LazTVMousemove(Sender: TObject; {%H-}Shift: TShiftState;
                             X, Y: Integer);
    Procedure LazTVMouseWheel(Sender: TObject; Shift: TShiftState;
                   {%H-}WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
    procedure TreeViewKeyPress(Sender: TObject; var Key: char);
    procedure ResultsNoteBookPageChanged (Sender: TObject );
    procedure SearchInListChange(Sender: TObject );
    procedure TreeViewMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
  private
    FMaxItems: integer;
    FWorkedSearchText: string;
    FOnSelectionChanged: TNotifyEvent;
    FMouseOverIndex: integer;
    function BeautifyPageName(const APageName: string): string;
    function GetPageIndex(const APageName: string): integer;
    function GetTreeView(APageIndex: integer): TLazSearchResultTV;
    procedure SetItems(Index: Integer; Value: TStrings);
    function GetItems(Index: integer): TStrings;
    procedure SetMaxItems(const AValue: integer);
    procedure UpdateToolbar;
  public
    function AddSearch(const ResultsName: string;
                       const SearchText: string;
                       const ReplaceText: string;
                       const ADirectories: string;
                       const AMask: string;
                       const TheOptions: TLazFindInFileSearchOptions): TTabSheet;
    function GetSourcePositon: TPoint;
    function GetSourceFileName: string;
    function GetSelectedText: string;
    function GetSelectedMatchPos: TLazSearchMatchPos;
    procedure AddMatch(const APageIndex: integer;
                       const Filename: string; const StartPos, EndPos: TPoint;
                       const TheText: string;
                       const MatchStart: integer; const MatchLen: integer);
    procedure BeginUpdate(APageIndex: integer);
    procedure EndUpdate(APageIndex: integer);
    procedure Parse_Search_Phrases(var slPhrases: TStrings);
    procedure ClosePage(PageIndex: integer);

    property MaxItems: integer read FMaxItems write SetMaxItems;
    property WorkedSearchText: string read FWorkedSearchText;
    property OnSelectionChanged: TNotifyEvent read fOnSelectionChanged
                                              write fOnSelectionChanged;
    property Items[Index: integer]: TStrings read GetItems write SetItems;
  end;

var
  SearchResultsView: TSearchResultsView = nil;
  OnSearchResultsViewSelectionChanged: TNotifyEvent = nil;
  OnSearchAgainClicked: TNotifyEvent = nil;

implementation

{$R *.lfm}

const
  MaxTextLen = 80;

function CopySearchMatchPos(var Src, Dest: TLazSearchMatchPos): Boolean;
begin
  Result := False;
  if ((Src = nil) or (Dest = nil)) then Exit;
  Dest.MatchStart := Src.MatchStart;
  Dest.MatchLen := Src.MatchLen;
  Dest.Filename := Src.Filename;
  Dest.FileStartPos := Src.FileStartPos;
  Dest.FileEndPos := Src.FileEndPos;
  Dest.TheText := Src.TheText;
  Dest.ShownFilename := Src.ShownFilename;
  Result := True;
end;
  
function GetTreeSelectedItemsAsText(ATreeView: TCustomTreeView): string;
var
  sl: TStringList;
  node: TTreeNode;
begin
  sl:=TStringList.Create;
  node := ATreeView.GetFirstMultiSelected;
  while assigned(node) do
  begin
    sl.Add(node.Text);
    node := node.GetNextMultiSelected;
  end;
  Result:=sl.Text;
  sl.Free;
end;

{ TSearchResultsView }

procedure TSearchResultsView.Form1Create(Sender: TObject);
var
  CloseCommand: TIDECommand;
begin
  FMaxItems:=50000;
  ResultsNoteBook.Options:= ResultsNoteBook.Options+[nboShowCloseButtons];
  ResultsNoteBook.Update;

  Name:=NonModalIDEWindowNames[nmiwSearchResultsViewName];
  Caption:=lisMenuViewSearchResults;

  SearchAgainButton.Hint:=rsStartANewSearch;
  ClosePageButton.Hint := rsCloseCurrentPage;
  SearchInListEdit.Hint:=rsFilterTheListWithString;
  CloseCommand := IDECommandList.FindIDECommand(ecClose);
  if CloseCommand <> nil then
  begin
    if CloseCommand.AsShortCut <> 0 then
      actClosePage.ShortCut:=CloseCommand.AsShortCut;
    if (CloseCommand.ShortcutB.Key1 <> 0) and (CloseCommand.ShortcutB.Key2 = 0) then
      actClosePage.SecondaryShortCuts.Append(ShortCutToText(
        ShortCut(CloseCommand.ShortcutB.Key1, CloseCommand.ShortcutB.Shift1)));
  end;
  fOnSelectionChanged:= nil;
  ShowHint:= True;
  fMouseOverIndex:= -1;

  mniCopyItem.Caption := lisCopyItemToClipboard;
  mniCopySelected.Caption := lisCopySelectedItemToClipboard;
  mniCopyAll.Caption := lisCopyAllItemsToClipboard;
  mniExpandAll.Caption := lisExpandAll;
  mniCollapseAll.Caption := lisCollapseAll;
end;

procedure TSearchResultsView.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  // Using a dock manager...
  if Parent<>nil then
  begin
    CloseAction := caNone;
    //todo: helper function in DockManager or IDEDockMaster for closing forms.
    // Only close the window if it's floating.
    // AnchorDocking doesn't seem to initialize 'FloatingDockSiteClass' so we can't just check 'Floating'.
    // Also, AnchorDocking use nested forms, so the check for HostDockSite.Parent.
    if Assigned(HostDockSite) and (HostDockSite.DockClientCount <= 1)
      and (HostDockSite is TCustomForm) and (HostDockSite.Parent = nil) then
    begin
      TCustomForm(HostDockSite).Close;
    end;
  end;
end;

procedure TSearchResultsView.FormKeyDown(Sender: TObject; var Key: Word; 
  Shift: TShiftState);
begin
  if (Key = VK_ESCAPE) then
  begin
    Key := VK_UNKNOWN;
    Close;
  end;  
end;

procedure TSearchResultsView.mniCopyAllClick(Sender: TObject);
var
  sl: TStrings;
begin
  sl := (popList.PopupComponent as TLazSearchResultTV).ItemsAsStrings;
  Clipboard.AsText := sl.Text;
  sl.Free;
end;

procedure TSearchResultsView.mniCopyItemClick(Sender: TObject);
var
  tv: TCustomTreeView;
  node: TTreeNode;
begin
  tv := popList.PopupComponent as TCustomTreeView;
  with tv.ScreenToClient(popList.PopupPoint) do
    node := tv.GetNodeAt(X, Y);
  if node <> nil then
    Clipboard.AsText := node.Text;
end;

procedure TSearchResultsView.mniCopySelectedClick(Sender: TObject);
begin
  Clipboard.AsText := GetTreeSelectedItemsAsText(popList.PopupComponent as TCustomTreeView);
end;

procedure TSearchResultsView.mniExpandAllClick(Sender: TObject);
var
  CurrentTV: TLazSearchResultTV;
  Key: Char = '*';
begin
  CurrentTV := GetTreeView(ResultsNoteBook.PageIndex);
  if Assigned(CurrentTV) then
    TreeViewKeyPress(CurrentTV, Key);
end;

procedure TSearchResultsView.mniCollapseAllClick(Sender: TObject);
var
  CurrentTV: TLazSearchResultTV;
  Key: Char = '/';
begin
  CurrentTV := GetTreeView(ResultsNoteBook.PageIndex);
  if Assigned(CurrentTV) then
    TreeViewKeyPress(CurrentTV, Key);
end;

procedure TSearchResultsView.ResultsNoteBookMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  TabIndex: LongInt;
begin
  if (Button = mbMiddle) then
  begin
    TabIndex := ResultsNoteBook.IndexOfPageAt(Point(X,Y));
    if TabIndex >= 0 then
      ResultsNoteBookClosetabclicked(ResultsNoteBook.Page[TabIndex]);
  end;
end;

procedure TSearchResultsView.ClosePageButtonClick(Sender: TObject);
begin
  ClosePage(ResultsNoteBook.PageIndex);
end;

procedure TSearchResultsView.actNextPageExecute(Sender: TObject);
begin
  ResultsNoteBook.SelectNextPage(True);
end;

procedure TSearchResultsView.actPrevPageExecute(Sender: TObject);
begin
  ResultsNoteBook.SelectNextPage(False);
end;

{Keeps track of the Index of the Item the mouse is over, Sets ShowHint to true
if the Item length is longer than the TreeView client width.}
procedure TSearchResultsView.LazTVMousemove(Sender: TObject; Shift: TShiftState;
                                       X, Y: Integer);
var
  Node: TTreeNode;
begin
  if Sender is TLazSearchResultTV then
  begin
    with Sender as TLazSearchResultTV do
    begin
      Node := GetNodeAt(X, Y);
      if Assigned(Node) then
        fMouseOverIndex:=Node.Index
      else
        fMouseOverIndex:=-1;
      if (fMouseOverIndex > -1) and (fMouseOverIndex < Items.Count)
      and (Canvas.TextWidth(Items[fMouseOverIndex].Text) > Width) then
        ShowHint:= True
      else
        ShowHint:= False;
    end;//with
  end;//
end;//LazTVMousemove

{Keep track of the mouse position over the treeview when the wheel is used}
procedure TSearchResultsView.LazTVMouseWheel(Sender: TObject;
  Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
  var Handled: Boolean);
begin
  LazTVMouseMove(Sender,Shift,MousePos.X, MousePos.Y);
  Handled:= false;
end;

procedure TSearchResultsView.TreeViewKeyPress(Sender: TObject; var Key: char);
var
  i: Integer;
  Tree: TLazSearchResultTV;
  Node: TTreeNode;
  Collapse: Boolean;
begin
  if Key in ['/', '*'] then
  begin
    Collapse := Key = '/';
    Tree := (Sender as TLazSearchResultTV);
    for i := Tree.Items.TopLvlCount -1 downto 0 do
    begin
      Node := Tree.Items.TopLvlItems[i];
      if Collapse then
        Node.Collapse(False)
      else
        Node.Expand(False);
    end;
    Key := #0;
  end else
  if Key = Char(VK_RETURN) then  //SearchInListEdit passes only OnPress through
  begin
    Key := #0;
    if Assigned(FOnSelectionChanged) then
      FOnSelectionChanged(Self);
  end;
end;

procedure TSearchResultsView.ResultsNoteBookPageChanged (Sender: TObject );
var
  CurrentTV: TLazSearchResultTV;
begin
  CurrentTV := GetTreeView(ResultsNoteBook.PageIndex);
  if Assigned(CurrentTV) and not (csDestroying in CurrentTV.ComponentState) then begin
    SearchInListEdit.FilteredTreeview := CurrentTV;
    SearchInListEdit.Filter := CurrentTV.SearchInListPhrases;
  end;
  UpdateToolbar;
end;

procedure TSearchResultsView.SearchInListChange (Sender: TObject );
var
  CurrentTV: TLazSearchResultTV;
begin
  CurrentTV := GetTreeView(ResultsNoteBook.PageIndex);
  if Assigned(CurrentTV) then
    CurrentTV.SearchInListPhrases := SearchInListEdit.Text;
end;

procedure TSearchResultsView.TreeViewMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  TV: TCustomTreeView;
  Node: TTreeNode;
begin
  if Button<>mbLeft then exit;
  TV:=Sender as TCustomTreeView;
  Node:=TV.GetNodeAt(X,Y);
  if Node=nil then exit;
  if x<Node.DisplayTextLeft then exit;
  //debugln(['TSearchResultsView.TreeViewMouseDown single=',([ssDouble,ssTriple,ssQuad]*Shift=[]),' Option=',EnvironmentOptions.MsgViewDblClickJumps]);
  if EnvironmentOptions.MsgViewDblClickJumps then
  begin
    // double click jumps
    if not (ssDouble in Shift) then exit;
  end else begin
    // single click jumps -> single selection
    if ([ssDouble,ssTriple,ssQuad]*Shift<>[]) then exit;
    TV.Items.SelectOnlyThis(Node);
  end;
  if Assigned(fOnSelectionChanged) then
    fOnSelectionChanged(Self);
end;

function TSearchResultsView.BeautifyPageName(const APageName: string): string;
const
  MaxPageName = 25;
begin
  Result:=SpecialCharsToHex(APageName);
  if UTF8Length(Result)>MaxPageName then
    Result:=UTF8Copy(Result,1,MaxPageName-5)+'...';
end;

procedure TSearchResultsView.AddMatch(const APageIndex: integer;
  const Filename: string; const StartPos, EndPos: TPoint;
  const TheText: string;
  const MatchStart: integer; const MatchLen: integer);
var
  CurrentTV: TLazSearchResultTV;
  SearchPos: TLazSearchMatchPos;
  ShownText: String;
  LastPos: TLazSearchMatchPos;
begin
  CurrentTV:=GetTreeView(APageIndex);
  if Assigned(CurrentTV) then
  begin
    if CurrentTV.Updating then begin
      if CurrentTV.UpdateItems.Count>=MaxItems then begin
        CurrentTV.Skipped:=CurrentTV.Skipped+1;
        exit;
      end;
    end else begin
      if CurrentTV.Items.Count>=MaxItems then begin
        CurrentTV.Skipped:=CurrentTV.Skipped+1;
        exit;
      end;
    end;

    SearchPos:= TLazSearchMatchPos.Create;
    SearchPos.MatchStart:=MatchStart;
    SearchPos.MatchLen:=MatchLen;
    SearchPos.Filename:=Filename;
    SearchPos.FileStartPos:=StartPos;
    SearchPos.FileEndPos:=EndPos;
    SearchPos.TheText:=TheText;
    SearchPos.ShownFilename:=SearchPos.Filename;
    ShownText:=CurrentTV.BeautifyLine(SearchPos);
    LastPos:=nil;
    if CurrentTV.Updating then begin
      if (CurrentTV.UpdateItems.Count>0)
      and (CurrentTV.UpdateItems.Objects[CurrentTV.UpdateItems.Count-1] is TLazSearchMatchPos) then
        LastPos:=TLazSearchMatchPos(CurrentTV.UpdateItems.Objects[CurrentTV.UpdateItems.Count-1]);
    end else
      if (CurrentTV.Items.Count>0) and Assigned(CurrentTV.Items[CurrentTV.Items.Count-1].Data) then
        LastPos:=TLazSearchMatchPos(CurrentTV.Items[CurrentTV.Items.Count-1].Data);
    if (LastPos<>nil) and (LastPos.Filename=SearchPos.Filename) and
       (LastPos.FFileStartPos.Y=SearchPos.FFileStartPos.Y) and
       (LastPos.FFileEndPos.Y=SearchPos.FFileEndPos.Y) then
    begin
      while (LastPos.NextInThisLine<>nil) do
        LastPos := LastPos.NextInThisLine;
      LastPos.NextInThisLine:=SearchPos
    end
    else if CurrentTV.Updating then
      CurrentTV.UpdateItems.AddObject(ShownText, SearchPos)
    else
      CurrentTV.AddNode(ShownText, SearchPos);
    CurrentTV.ShortenPaths;
  end;//if
end;//AddMatch

procedure TSearchResultsView.BeginUpdate(APageIndex: integer);
var
  CurrentTV: TLazSearchResultTV;
begin
  CurrentTV:= GetTreeView(APageIndex);
  if Assigned(CurrentTV) then
    CurrentTV.BeginUpdate;
  UpdateToolbar;
end;

procedure TSearchResultsView.EndUpdate(APageIndex: integer);
var
  CurrentTV: TLazSearchResultTV;
begin
  CurrentTV:= GetTreeView(APageIndex);
  if Assigned(CurrentTV) then
  begin
    CurrentTV.EndUpdate;
    if CurrentTV.Items.Count>0 then begin
      CurrentTV.Items[0].Selected:=True;
    end;
  end;
  UpdateToolbar;
end;

procedure TSearchResultsView.Parse_Search_Phrases(var slPhrases: TStrings);
var i, iLength: Integer;
    sPhrases, sPhrase: string;
begin
 //Parse Phrases
 sPhrases := SearchInListEdit.Text;
 iLength := Length(sPhrases);
 sPhrase := '';
 for i:=1 to iLength do
  begin
   if ((sPhrases[i] = ' ') or (sPhrases[i] = ',') or (i = iLength)) then
    begin
     if not ((sPhrases[i] = ' ') or (sPhrases[i] = ',')) then
      sPhrase := sPhrase + sPhrases[i];
     if (sPhrase > ' ') then
      slPhrases.Add(UpperCase(sPhrase));//End of phrase, add to phrase list
     sPhrase := '';//Reset sPhrase
    end else
    begin
     if (sPhrases[i] > ' ') then
      sPhrase := sPhrase + sPhrases[i];
    end;//End if ((sPhrases[i] = ' ') or (sPhrases[i] = ','))
  end;//End for-loop i
end;

procedure TSearchResultsView.ClosePage(PageIndex: integer);
var
  CurrentTV: TLazSearchResultTV;
begin
  if (PageIndex>=0) and (PageIndex<ResultsNoteBook.PageCount) then
  begin
    CurrentTV:= GetTreeView(PageIndex);
    if Assigned(CurrentTV) and CurrentTV.Updating then
      exit;

    ResultsNoteBook.Pages[PageIndex].Free;
  end;
  if ResultsNoteBook.PageCount = 0 then
    Close;
end;

{Sets the Items from the treeview on the currently selected page in the TNoteBook}
procedure TSearchResultsView.SetItems(Index: Integer; Value: TStrings);
var
  CurrentTV: TLazSearchResultTV;
begin
  if Index > -1 then
  begin
    CurrentTV:= GetTreeView(Index);
    if Assigned(CurrentTV) then
    begin
      if CurrentTV.Updating then
        CurrentTV.UpdateItems.Assign(Value)
      else
        CurrentTV.Items.Assign(Value);
      CurrentTV.Skipped:=0;
    end;
  end;
end;

function TSearchResultsView.GetItems(Index: integer): TStrings;
var
  CurrentTV: TLazSearchResultTV;
begin
  result:= nil;
  CurrentTV:= GetTreeView(Index);
  if Assigned(CurrentTV) then
  begin
    if CurrentTV.Updating then
      result:= CurrentTV.UpdateItems
    else
      Result := CurrentTV.ItemsAsStrings;
  end;
end;

procedure TSearchResultsView.SetMaxItems(const AValue: integer);
begin
  if FMaxItems=AValue then exit;
  FMaxItems:=AValue;
end;

procedure TSearchResultsView.UpdateToolbar;
var
  CurrentTV: TLazSearchResultTV;
  state: Boolean;
begin
  CurrentTV:= GetTreeView(ResultsNoteBook.PageIndex);
  state := Assigned(CurrentTV) and not CurrentTV.Updating;
  SearchAgainButton.Enabled := state;
  ClosePageButton.Enabled := state;
  SearchInListEdit.Enabled := state;
end;

procedure TSearchResultsView.ResultsNoteBookClosetabclicked(Sender: TObject);
begin
  if (Sender is TTabSheet) then
    ClosePage(TTabSheet(Sender).PageIndex)
end;

procedure TSearchResultsView.SearchAgainButtonClick(Sender: TObject);
var
  CurrentTV: TLazSearchResultTV;
  SearchObj: TLazSearch;
begin
  CurrentTV:= GetTreeView(ResultsNoteBook.PageIndex);
  if not Assigned(CurrentTV) then begin
    MainIDEInterface.FindInFilesPerDialog(Project1);
  end
  else begin
    SearchObj:= CurrentTV.SearchObject;
    OnSearchAgainClicked(SearchObj);
    MainIDEInterface.FindInFiles(Project1, SearchObj.SearchString);
  end;
end;

procedure TSearchResultsView.TreeViewKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key = VK_RETURN) and (Shift = []) then
  begin
    Key:=VK_UNKNOWN;
    if Assigned(FOnSelectionChanged) then
      FOnSelectionChanged(Self);
  end;     
end;

{ Add Result will create a tab in the Results view window with an new
  treeview or focus an existing TreeView and update it's searchoptions.}
function TSearchResultsView.AddSearch(const ResultsName: string;
  const SearchText: string;
  const ReplaceText: string;
  const ADirectories: string;
  const AMask: string;
  const TheOptions: TLazFindInFileSearchOptions): TTabSheet;
var
  NewTreeView: TLazSearchResultTV;
  NewPage: LongInt;
  SearchObj: TLazSearch;
begin
  Result:= nil;
  if Assigned(ResultsNoteBook) then
    with ResultsNoteBook do
    begin
      FWorkedSearchText:=BeautifyPageName(ResultsName);
      NewPage:= TCustomTabControl(ResultsNoteBook).Pages.Add(FWorkedSearchText);
      PageIndex:= NewPage;
      Page[PageIndex].OnKeyDown := @TreeViewKeyDown;
      if NewPage > -1 then
      begin
        NewTreeView:= TLazSearchResultTV.Create(Page[NewPage]);
        with NewTreeView do
        begin
          Parent:= Page[NewPage];
          Align:= alClient;
          BorderSpacing.Around := 0;
          OnKeyDown := @TreeViewKeyDown;
          OnAdvancedCustomDrawItem:= @TreeViewAdvancedCustomDrawItem;
          OnShowHint:= @LazTVShowHint;
          OnMouseMove:= @LazTVMousemove;
          OnMouseWheel:= @LazTVMouseWheel;
          OnMouseDown:=@TreeViewMouseDown;
          OnKeyPress:=@TreeViewKeyPress;
          ShowHint:= true;
          RowSelect := True;                        // we are using custom draw
          Options := Options + [tvoAllowMultiselect] - [tvoThemedDraw];
          PopupMenu := popList;
          NewTreeView.Canvas.Brush.Color:= clWhite;
        end;//with
        SearchObj:=NewTreeView.SearchObject;
        if SearchObj<>nil then begin
          SearchObj.SearchString:= SearchText;
          SearchObj.ReplaceText := ReplaceText;
          SearchObj.SearchDirectories:= ADirectories;
          SearchObj.SearchMask:= AMask;
          SearchObj.SearchOptions:= TheOptions;
        end;
        NewTreeView.Skipped:=0;
      end
      else
        NewTreeView:=nil;
      Result:= Pages[PageIndex];
      SearchInListEdit.Text:='';
      SearchInListEdit.Filter:='';
      SearchInListEdit.FilteredTreeview := NewTreeView;
    end;//with
end;//AddResult

procedure TSearchResultsView.LazTVShowHint(Sender: TObject; HintInfo: PHintInfo);
var
  MatchPos: TLazSearchMatchPos;
  HintStr: string;
begin
  if Sender is TLazSearchResultTV then
  begin
    With Sender as TLazSearchResultTV do
    begin
      if (fMouseOverIndex >= 0) and (fMouseOverIndex < Items.Count) then
      begin
        if Assigned(Items[fMouseOverIndex].Data) then
          MatchPos:= TLazSearchMatchPos(Items[fMouseOverIndex].Data)
        else
          MatchPos:= nil;
        if MatchPos<>nil then
          HintStr:=MatchPos.Filename
                   +' ('+IntToStr(MatchPos.FileStartPos.Y)
                   +','+IntToStr(MatchPos.FileStartPos.X)+')'
                   +' '+MatchPos.TheText
        else
          HintStr:=Items[fMouseOverIndex].Text;
        Hint:= HintStr;
      end;//if
    end;//with
  end;//if
end;//LazTVShowHint

procedure TSearchResultsView.TreeViewAdvancedCustomDrawItem(
  Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState;
  Stage: TCustomDrawStage; var PaintImages, DefaultDraw: Boolean);
var
  CurPart: string;
  TheTop: integer;
  MatchObj: TObject;
  MatchPos,FirstMatchPos: TLazSearchMatchPos;
  TextEnd, DrawnTextLength: integer;
  ARect: TRect;
  TV: TLazSearchResultTV;
begin
  if Stage <> cdPostPaint then Exit;

  TV:=Sender as TLazSearchResultTV;
  if [cdsSelected,cdsMarked] * State <> [] then
    TV.Canvas.Font.Color := clHighlightText;

  ARect:=Node.DisplayRect(true);
  TV.Canvas.FillRect(ARect);

  MatchObj := TLazSearchMatchPos(Node.Data);
  if assigned(MatchObj) and (MatchObj is TLazSearchMatchPos) then
    MatchPos:= TLazSearchMatchPos(Node.Data)
  else
    MatchPos:= nil;

  if Assigned(MatchPos) then
  begin

    FirstMatchPos:=MatchPos;
    TheTop:= ARect.Top;
    TextEnd:=ARect.Left;
    DrawnTextLength:=0;

    CurPart:=MatchPos.ShownFilename+' ('+IntToStr(MatchPos.FileStartPos.Y)
        +':'+IntToStr(MatchPos.FileStartPos.X);
    MatchPos:=MatchPos.NextInThisLine;
    SetBkMode(TV.Canvas.Handle, TRANSPARENT);
    while assigned(MatchPos) do begin
      CurPart:=CurPart+','+IntToStr(MatchPos.FileStartPos.X);
      MatchPos:=MatchPos.NextInThisLine;
    end;
    CurPart:=CurPart+') ';
    TV.Canvas.TextOut(TextEnd, TheTop, CurPart);
    TextEnd:= TextEnd + TV.Canvas.TextWidth(CurPart);

    MatchPos:=FirstMatchPos;
    while assigned(MatchPos) do begin
      //debugln(['TSearchResultsView.TreeViewAdvancedCustomDrawItem MatchPos.TheText="',MatchPos.TheText,'" MatchPos.MatchStart=',MatchPos.MatchStart,' MatchPos.MatchLen=',MatchPos.MatchLen]);
      // draw normal text
      CurPart:=SpecialCharsToHex(copy(MatchPos.TheText,DrawnTextLength+1,MatchPos.MatchStart-1-DrawnTextLength));
      DrawnTextLength:=MatchPos.MatchStart-1;
      TV.Canvas.TextOut(TextEnd, TheTop, CurPart);
      TextEnd:= TextEnd + TV.Canvas.TextWidth(CurPart);

      // draw found text (matched)
      CurPart:=SpecialCharsToHex(copy(MatchPos.TheText,DrawnTextLength+1,MatchPos.MatchLen));
      DrawnTextLength:=DrawnTextLength+MatchPos.MatchLen;
      if UTF8Length(CurPart)>MaxTextLen then
        CurPart:=UTF8Copy(CurPart,1,MaxTextLen)+'...';
      TV.Canvas.Font.Style:= TV.Canvas.Font.Style + [fsBold];
      TV.Canvas.TextOut(TextEnd, TheTop, CurPart);
      TextEnd:= TextEnd + TV.Canvas.TextWidth(CurPart);
      TV.Canvas.Font.Style:= TV.Canvas.Font.Style - [fsBold];

      if MatchPos.NextInThisLine=nil then begin
        CurPart:=SpecialCharsToHex(copy(MatchPos.TheText, DrawnTextLength+1,Length(MatchPos.TheText)));
        TV.Canvas.TextOut(TextEnd, TheTop, CurPart);
      end;
      MatchPos:=MatchPos.NextInThisLine;
    end;
  end
  else begin
    // this is usually the filename only
    // draw it here too, so that the correct colors are used
    TV.Canvas.TextOut(ARect.Left, ARect.Top, Node.Text);
  end;//if
end;//TreeViewDrawItem

{Returns the Position within the source file from a properly formated search result}
function TSearchResultsView.GetSourcePositon: TPoint;
var
  MatchPos: TLazSearchMatchPos;
begin
  Result.x:= -1;
  Result.y:= -1;
  MatchPos:=GetSelectedMatchPos;
  if MatchPos=nil then exit;
  Result:=MatchPos.FileStartPos;
end;//GetSourcePositon

{Returns The file name portion of a properly formated search result}
function TSearchResultsView.GetSourceFileName: string;
var
  MatchPos: TLazSearchMatchPos;
begin
  MatchPos:=GetSelectedMatchPos;
  if MatchPos=nil then
    Result:=''
  else
    Result:=MatchPos.Filename;
end;//GetSourceFileName

{Returns the selected text in the currently active TreeView.}
function TSearchResultsView.GetSelectedText: string;
var
  ThePage: TTabSheet;
  TheTreeView: TLazSearchResultTV;
  i: integer;
begin
  result:= '';
  i:= ResultsNoteBook.PageIndex;
  if i > -1 then
  begin
    ThePage:= ResultsNoteBook.Pages[i];
    if Assigned(ThePage) then
    begin
      TheTreeView:= GetTreeView(ThePage.PageIndex);
      if Assigned(TheTreeView.Selected) then
        Result:= TheTreeView.Selected.Text;
    end;//if
  end;//if
end;//GetSelectedText

function TSearchResultsView.GetSelectedMatchPos: TLazSearchMatchPos;
var
  ThePage: TTabSheet;
  TheTreeView: TLazSearchResultTV;
  i: integer;
begin
  Result:= nil;
  i:= ResultsNoteBook.PageIndex;
  if i > -1 then
  begin
    ThePage:= ResultsNoteBook.Pages[i];
    if Assigned(ThePage) then
    begin
      TheTreeView:= GetTreeView(ThePage.PageIndex);
      if Assigned(TheTreeView.Selected) then
        Result := TLazSearchMatchPos(TheTreeView.Selected.Data);
    end;
  end;
end;

function TSearchResultsView.GetPageIndex(const APageName: string): integer;
var
  Paren, i: integer;
  PN: String;
begin
  Result:= -1;
  for i:= 0 to ResultsNoteBook.PageCount - 1 do
  begin
    PN:= ResultsNoteBook.Page[i].Caption;
    Paren:= Pos(' (', PN);
    if (Paren>0) and (PosEx(')', PN, Paren+2)>0) then
      PN:= LeftStr(PN, Paren-1);
    if PN = APageName then
    begin
      Result:= i;
      break;
    end;
  end;
end;

{Returns a the TreeView control from a Tab if both the page and the TreeView
 exist else returns nil}
function TSearchResultsView.GetTreeView(APageIndex: integer): TLazSearchResultTV;
var
  i: integer;
  ThePage: TTabSheet;
begin
  Result:= nil;
  if (APageIndex > -1) and (APageIndex < ResultsNoteBook.PageCount) then
  begin
    ThePage:= ResultsNoteBook.Pages[APageIndex];
    if Assigned(ThePage) then
    begin
      for i:= 0 to ThePage.ComponentCount - 1 do
      begin
        if ThePage.Components[i] is TLazSearchResultTV then
        begin
          result:= TLazSearchResultTV(ThePage.Components[i]);
          break;
        end;
      end;
    end;
  end;
end;

procedure TLazSearchResultTV.SetSkipped(const AValue: integer);
var
  SrcList: TStrings;
  s: String;
  HasSkippedLine: Boolean;
  SkippedLine: String;
begin
  if FSkipped=AValue then exit;
  FSkipped:=AValue;
  s:=rsFoundButNotListedHere;
  if fUpdating then
    SrcList:=fUpdateStrings
  else
    SrcList:=ItemsAsStrings;
  if (SrcList.Count>0) and (copy(SrcList[SrcList.Count-1],1,length(s))=s) then
    HasSkippedLine:=true
  else
    HasSkippedLine:=false;
  SkippedLine:=s+IntToStr(FSkipped);
  if FSkipped>0 then begin
    if HasSkippedLine then begin
      SrcList[SrcList.Count-1]:=SkippedLine;
    end else begin
      SrcList.add(SkippedLine);
    end;
  end else begin
    if HasSkippedLine then
      SrcList.Delete(SrcList.Count-1);
  end;
end;

procedure TLazSearchResultTV.AddNode(Line: string; MatchPos: TLazSearchMatchPos);
var
  Node: TTreeNode;
  ChildNode: TTreeNode;
begin
  if MatchPos=nil then exit;
  Node := Items.FindNodeWithText(MatchPos.FileName);

  //enter a new file entry
  if not Assigned(Node) then
    Node := Items.Add(Node, MatchPos.FileName);

  ChildNode := Items.AddChild(Node, Line);
  Node.Expanded:=true;
  ChildNode.Data := MatchPos;
end;

{******************************************************************************
  TLazSearchResultTV
******************************************************************************}
Constructor TLazSearchResultTV.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ReadOnly := True;
  fSearchObject:= TLazSearch.Create;
  fUpdating:= false;
  fUpdateCount:= 0;
  fUpdateStrings:= TStringList.Create;
  FSearchInListPhrases := '';
  fFiltered := False;
end;//Create

Destructor TLazSearchResultTV.Destroy;
begin
  if Assigned(fSearchObject) then
    FreeAndNil(fSearchObject);
  //if UpdateStrings is empty, the objects are stored in Items due to filtering
  //filtering clears UpdateStrings
  if (fUpdateStrings.Count = 0) then
   FreeObjectsTN(Items);
  if Assigned(fUpdateStrings) then
  begin
    FreeObjects(fUpdateStrings);
    FreeAndNil(fUpdateStrings);
  end;
  inherited Destroy;
end;//Destroy

procedure TLazSearchResultTV.BeginUpdate;
var
  s: TStrings;
begin
  inc(fUpdateCount);
  if (fUpdateCount = 1) then
  begin
    // save old treeview content
    if Assigned(Items) then
    begin
      s := ItemsAsStrings;
      fUpdateStrings.Assign(s);
      s.Free;
    end;
    fUpdating:= true;
  end;
end;

procedure TLazSearchResultTV.EndUpdate;
var
  i: integer;
begin
  if (fUpdateCount = 0) then
    RaiseGDBException('TLazSearchResultTV.EndUpdate');

  Dec(fUpdateCount);
  if (fUpdateCount = 0) then
  begin
    ShortenPaths;
    fUpdating:= false;
    FreeObjectsTN(Items);

    Items.BeginUpdate;
    Items.Clear;

    for i := 0 to fUpdateStrings.Count - 1 do
      AddNode(fUpdateStrings[i], TLazSearchMatchPos(fUpdateStrings.Objects[i]));

    Items.EndUpdate;
  end;//if
end;//EndUpdate

procedure TLazSearchResultTV.ShortenPaths;
var
  i: Integer;
  AnObject: TObject;
  SharedPath: String;
  MatchPos: TLazSearchMatchPos;
  SrcList: TStrings;
  SharedLen: Integer;
  ShownText: String;
  FreeSrcList: Boolean;
begin
  if fUpdateCount>0 then exit;

  if fUpdating then begin
    SrcList:=fUpdateStrings;
    FreeSrcList:=false;
  end else begin
    SrcList:=ItemsAsStrings;
    FreeSrcList:=true;
  end;
  try

    // find shared path (the path of all filenames, that is the same)
    SharedPath:='';
    for i:=0 to SrcList.Count-1 do begin
      AnObject:=SrcList.Objects[i];
      if AnObject is TLazSearchMatchPos then begin
        MatchPos:=TLazSearchMatchPos(AnObject);
        if i=0 then
          SharedPath:=ExtractFilePath(MatchPos.Filename)
        else if (SharedPath<>'') then begin
          SharedLen:=0;
          while (SharedLen<length(MatchPos.Filename))
          and (SharedLen<length(SharedPath))
          and (MatchPos.Filename[SharedLen+1]=SharedPath[SharedLen+1])
          do
            inc(SharedLen);
          while (SharedLen>0) and (SharedPath[SharedLen]<>PathDelim) do
            dec(SharedLen);
          if SharedLen<>length(SharedPath) then
            SharedPath:=copy(SharedPath,1,SharedLen);
        end;
      end;
    end;

    // shorten shown paths
    SharedLen:=length(SharedPath);
    for i:=0 to SrcList.Count-1 do begin
      AnObject:=SrcList.Objects[i];
      if AnObject is TLazSearchMatchPos then begin
        MatchPos:=TLazSearchMatchPos(AnObject);
        MatchPos.ShownFilename:=copy(MatchPos.Filename,SharedLen+1,
                                     length(MatchPos.Filename));
        ShownText:=BeautifyLine(MatchPos);
        SrcList[i]:=ShownText;
        SrcList.Objects[i]:=MatchPos;
      end;
    end;
  finally
    if FreeSrcList then SrcList.Free;
  end;
end;

procedure TLazSearchResultTV.FreeObjectsTN(tnItems: TTreeNodes);
var i: Integer;
begin
 for i:=0 to tnItems.Count-1 do
   if Assigned(tnItems[i].Data) then
     TLazSearchMatchPos(tnItems[i].Data).Free;
end;

procedure TLazSearchResultTV.FreeObjects(slItems: TStrings);
var i: Integer;
begin
 if (slItems.Count <= 0) then Exit;
 for i:=0 to slItems.Count-1 do
  begin
   if Assigned(slItems.Objects[i]) then
    slItems.Objects[i].Free;
  end;//End for-loop
end;

function TLazSearchResultTV.BeautifyLine(const Filename: string; X, Y: integer;
  const Line: string): string;
begin
  Result:=SpecialCharsToHex(Line);
  if UTF8Length(Result)>MaxTextLen then
    Result:=UTF8Copy(Result,1,MaxTextLen)+'...';
  Result:=Filename
          +' ('+IntToStr(Y)
          +','+IntToStr(X)+')'
          +' '+Result;
end;

function TLazSearchResultTV.BeautifyLine(SearchPos: TLazSearchMatchPos): string;
begin
  Result:=BeautifyLine(SearchPos.ShownFilename,SearchPos.FileStartPos.X,
                       SearchPos.FileStartPos.Y,SearchPos.TheText);
end;

function TLazSearchResultTV.ItemsAsStrings: TStrings;
var
  i: integer;
begin
  Result := TStringList.Create;

  for i := 0 to Items.Count - 1 do
    Result.AddObject(Items[i].Text,TObject(Items[i].Data));
end;

{ TLazSearchMatchPos }

destructor TLazSearchMatchPos.Destroy;
begin
  FreeAndNil(FNextInThisLine);
  inherited Destroy;
end;

end.

