{
 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  Author: Mattias Gaertner
  
  Abstract:
    Interface to various IDE tools manipulating text.
}
unit TextTools;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  {$IF FPC_FULLVERSION >= 30200}System.{$ENDIF}UITypes,
  // LCL
  LCLType;

  { Sorting }
type
  TSortDirection = (sdAscending, sdDescending);
  TSortDomain = (sdWords, sdLines, sdParagraphs);

  TShowSortSelectionDialogFunc = function(const TheText: string;
    Highlighter: TObject; var SortedText: string): TModalResult;
  TSortTextFunc = function(const TheText: string; Direction: TSortDirection;
    Domain: TSortDomain; CaseSensitive, IgnoreSpace: boolean): string;

var
  ShowSortSelectionDialogFunc: TShowSortSelectionDialogFunc;
  SortTextFunc: TSortTextFunc;

  { Regular expressions
  
    This is a simple interface to regular expressions. The syntax is similar
    to Perl regular expressions. An illegal pattern will raise an Exception.
    
    Important: These functions are not thread safe!

    REMatches - function to test a regular expression.
    REVar - function to read the bracket values, found in the last call
            of REMatches.
    The ModifierStr sets the default values of r.e.syntax modifiers. Modifiers
    in r.e. (?ismx-ismx) will replace this default values.
    If you try to set unsupported modifier, an exception is raised

     Modifier /i - caseinsensitive, initialized from RegExprModifierI
     Modifier /s - '.' works as any char (else as [^\n]),
     Modifier /g - Turns all operators to non-greedy. e.g. '*' works as '*?',
                   all '+' as '+?' and so on.
     Modifier /m - Treat string as multiple lines. That is, change `^' and `$'
                   from matching at only the very start or end of the string to
                   the start or end of any line anywhere within the string.

    Examples:
      if REMatches('Lazarus','aza') then ...

      if REMatches('Lazarus','a(.)a','i') then
        s:=REVar(1); // this will be the 'z'
  }
  
var
  REException: ExceptClass; // initialized by the IDE
  
function REMatches(const TheText, RegExpr: string;
                const ModifierStr: string = ''; StartPos: integer = 1): boolean;
function REVar(Index: Integer): string; // 1 is the first
procedure REVarPos(Index: Integer; out MatchStart, MatchLength: integer);
function REVarCount: Integer;
function REReplace(const TheText, FindRegExpr, ReplaceRegExpr: string;
                    UseSubstutition: boolean;
                    const ModifierStr: string = ''): string;
function RESplit(const TheText, SeparatorRegExpr: string;
                 const ModifierStr: string = ''): TStrings;
procedure RESplit(const TheText, SeparatorRegExpr: string; Pieces: TStrings;
                  const ModifierStr: string = '');

// xml paths
function GetPathElement(const Path: string; StartPos: integer;
                        Stopper: char): string;

// For searching and filtering items in different lists.
function MultiWordSearch(aFilter, aText: string): boolean;
function KeyToQWERTY(var Key: Word; Shift: TShiftState; out aChar: char; aLowerCase: boolean = false): boolean;


//------------------------------------------------------------------------------
// Internal stuff.

type
  TREMatchesFunction = function(const TheText, RegExpr, ModifierStr: string;
                                StartPos: integer): boolean;
  TREVarFunction = function(Index: Integer): string;
  TREVarPosProcedure = procedure(Index: Integer;
                                 out MatchStart, MatchLength: integer);
  TREVarCountFunction = function: Integer;
  TREReplaceProcedure = function(const TheText, FindRegExpr,
                            ReplaceRegExpr: string; UseSubstutition: boolean;
                            const ModifierStr: string): string;
  TRESplitFunction = procedure(const TheText, SeparatorRegExpr: string;
                               Pieces: TStrings; const ModifierStr: string);
var
  REMatchesFunction: TREMatchesFunction = nil; // initialized by the IDE ...
  REVarFunction: TREVarFunction = nil;
  REVarPosProcedure: TREVarPosProcedure = nil;
  REVarCountFunction: TREVarCountFunction = nil;
  REReplaceProcedure: TREReplaceProcedure = nil;
  RESplitFunction: TRESplitFunction = nil;

implementation

function REMatches(const TheText, RegExpr: string;
  const ModifierStr: string; StartPos: integer): boolean;
begin
  Result:=REMatchesFunction(TheText,RegExpr,ModifierStr,StartPos);
end;

function REVar(Index: Integer): string;
begin
  Result:=REVarFunction(Index);
end;

procedure REVarPos(Index: Integer; out MatchStart, MatchLength: integer);
begin
  REVarPosProcedure(Index,MatchStart,MatchLength);
end;

function REVarCount: Integer;
begin
  Result:=REVarCountFunction();
end;

function REReplace(const TheText, FindRegExpr, ReplaceRegExpr: string;
  UseSubstutition: boolean; const ModifierStr: string): string;
begin
  Result:=REReplaceProcedure(TheText,FindRegExpr,ReplaceRegExpr,UseSubstutition,
                             ModifierStr);
end;

procedure RESplit(const TheText, SeparatorRegExpr: string; Pieces: TStrings;
  const ModifierStr: string);
begin
  RESplitFunction(TheText,SeparatorRegExpr,Pieces,ModifierStr);
end;

function RESplit(const TheText, SeparatorRegExpr: string;
  const ModifierStr: string): TStrings;
begin
  Result:=TStringList.Create;
  RESplit(TheText,SeparatorRegExpr,Result,ModifierStr);
end;

function GetPathElement(const Path: string; StartPos: integer;
  Stopper: char): string;
var
  p: LongInt;
begin
  p:=StartPos;
  while (p<=length(Path)) and (Path[p]<>Stopper) do inc(p);
  Result:=copy(Path,StartPos,p-StartPos);
end;

function MultiWordSearch(aFilter, aText: string): boolean;
var
  lExpressions: TStringList;
  i: Integer;

  function FilterByExpression(AFilter: string): boolean;
  var
    lConditions: TStringList;
    i: Integer;
  begin
    lConditions := TStringList.Create;
    try
      lConditions.QuoteChar := #0;
      lConditions.AddDelimitedText(AFilter, ' ', true);
      for i := 0 to lConditions.Count - 1 do
        if lConditions[i] <> '' then
        begin
          if lConditions[i][1] = '!' then
          begin
            lConditions[i] := RightStr(lConditions[i], length(lConditions[i]) - 1); // delete "!"
            if Pos(lConditions[i], aText) > 0 then
              exit(true);
          end else begin
            if Pos(lConditions[i], aText) <= 0 then
              exit(true);
          end;
        end;
      Result := false;
    finally
      FreeAndNil(lConditions);
    end;
  end;

begin
  if aFilter = '' then exit(true);
  aText := '"' + lowercase(aText) + '"';
  aFilter := lowercase(aFilter);

  lExpressions := TStringList.Create;
  try
    lExpressions.QuoteChar := #0;
    lExpressions.AddDelimitedText(aFilter, ',', true);
    for i := 0 to lExpressions.Count - 1 do
      if lExpressions[i] <> '' then
        if not FilterByExpression(lExpressions[i]) then
          exit(true);
    result := false;
  finally
    FreeAndNil(lExpressions);
  end;
end;

function KeyToQWERTY(var Key: Word; Shift: TShiftState; out aChar: char; aLowerCase: boolean = false): boolean;
begin
  aChar := #0;

  if Shift = [] then
    case Key of
      VK_A..VK_Z: aChar := chr(Key + $20); // VK-codes matches ASCII chars
      VK_LCL_COMMA: aChar := ',';
      VK_OEM_PERIOD: aChar := '.';
    end
  else if Shift = [ssShift] then
    case Key of
      VK_A..VK_Z:
        if aLowerCase
          then aChar := chr(Key + $20) // VK-codes matches ASCII chars
          else aChar := chr(Key);
      VK_LCL_MINUS: aChar := '_';
      VK_1        : aChar := '!';
      VK_LCL_QUOTE: aChar := '"';
    end;

  result := aChar <> #0;
  if result then
    Key := 0;
end;

end.

