Subversion Repositories fastphp

Rev

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

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