Subversion Repositories fastphp

Rev

Rev 22 | Rev 24 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

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