text: {
  /***************************************************************************
                             encloseselectiondlg.pas
                             -----------------------
  
   ***************************************************************************/
  
   ***************************************************************************
   *                                                                         *
   *   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: Dialog to setup parameters of the enclose selection function
  }
  unit EncloseSelectionDlg;
  
  {$mode objfpc}{$H+}
  
  interface
  
  uses
    Classes, SysUtils,
    // LCL
    Forms, Controls, Graphics, Dialogs, ExtCtrls, ButtonPanel,
    // LazUtils
    LazUTF8, LazTracer, LazStringUtils,
    // CodeTools
    BasicCodeTools, CodeToolManager, SourceChanger,
    // IDE
    LazarusIDEStrConsts;
  
  type
    TEncloseSelectionType = (
      estTryFinally,
      estTryExcept,
      estBeginEnd,
      estForBeginEnd,
      estWhileDoBeginEnd,
      estRepeatUntil,
      estWith,
      estPascalComment,
      estRegionArea
      );
      
    { TEncloseSelectionDialog }
  
    TEncloseSelectionDialog = class(TForm)
      ButtonPanel: TButtonPanel;
      TypeRadiogroup: TRADIOGROUP;
      procedure EncloseSelectionDialogCREATE(Sender: TObject);
    private
    public
      function GetEncloseType: TEncloseSelectionType;
    end;
    
  function ShowEncloseSelectionDialog(out TheType: TEncloseSelectionType
                                      ): TModalResult;
  function EncloseSelectionTypeDescription(TheType: TEncloseSelectionType
                                           ): string;
  procedure GetEncloseSelectionParams(TheType: TEncloseSelectionType;
                                      out Template: string);
  procedure EncloseTextSelection(const Template: string; Source: TStrings;
                                 SelectionStart, SelectionEnd: TPoint;
                                 out NewSelection: string; out NewCursor: TPoint);
  
  implementation
  
  {$R *.lfm}
  
  function EncloseSelectionTypeDescription(TheType: TEncloseSelectionType): string;
  begin
    Result:='';
    case TheType of
      estTryFinally: Result:='Try..Finally';
      estTryExcept: Result:='Try..Except';
      estBeginEnd: Result:='Begin..End';
      estForBeginEnd: Result:='For | do begin..end';
      estWhileDoBeginEnd: Result:='While | do begin..end';
      estRepeatUntil: Result:='Repeat..Until |';
      estWith: Result:='With | do begin..end';
      estPascalComment: Result:='{..}';
      estRegionArea: Result:='{$REGION ''|''}..{$ENDREGION}';
    else
      RaiseGDBException('EncloseSelectionTypeDescription');
    end;
  end;
  
  function ShowEncloseSelectionDialog(out TheType: TEncloseSelectionType
    ): TModalResult;
  var
    TheDialog: TEncloseSelectionDialog;
  begin
    TheType:=estBeginEnd;
    TheDialog:=TEncloseSelectionDialog.Create(nil);
    Result:=TheDialog.ShowModal;
    if Result=mrOk then
      TheType:=TheDialog.GetEncloseType;
    TheDialog.Free;
  end;
  
  procedure GetEncloseSelectionParams(TheType: TEncloseSelectionType; out
    Template: string);
  begin
    case TheType of
      estTryFinally:
        Template:='try'+LineEnding
                 +'  <selection>'+LineEnding
                 +'finally'+LineEnding
                 +'  |'+LineEnding
                 +'end;'+LineEnding;
  
      estTryExcept:
        Template:='try'+LineEnding
                 +'  <selection>'+LineEnding
                 +'except'+LineEnding
                 +'  |'+LineEnding
                 +'end;'+LineEnding;
  
      estBeginEnd:
        Template:='begin'+LineEnding
                 +'  |<selection>'+LineEnding
                 +'end;'+LineEnding;
  
      estForBeginEnd:
        Template:='for | do begin'+LineEnding
                 +'  <selection>'+LineEnding
                 +'end;'+LineEnding;
  
      estWhileDoBeginEnd:
        Template:='while | do begin'+LineEnding
                 +'  <selection>'+LineEnding
                 +'end;'+LineEnding;
  
      estRepeatUntil:
        Template:='repeat'+LineEnding
                 +'  <selection>'+LineEnding
                 +'until |;'+LineEnding;
  
      estWith:
        Template:='with | do begin'+LineEnding
                 +'  <selection>'+LineEnding
                 +'end;'+LineEnding;
  
      estPascalComment:
        Template:='{'+LineEnding
                 +'  |<selection>'+LineEnding
                 +'}'+LineEnding;
  
      estRegionArea:
        Template:='{$REGION ''|''}'+LineEnding
                 +'  <selection>'+LineEnding
                 +'{$ENDREGION}'+LineEnding;
  
    else
      RaiseGDBException('GetEnclosedSelectionParams');
    end;
  end;
  
  procedure EncloseTextSelection(const Template: string; Source: TStrings;
    SelectionStart, SelectionEnd: TPoint; out NewSelection: string; out
    NewCursor: TPoint);
  var
    TemplateLen: Integer;
    TemplatePos: Integer;
    LastWrittenTemplatePos: Integer;
    NewSelect: TMemoryStream;
    Y: Integer;
    X: Integer;
    OldSelectionIndent: Integer;
    TemplateIndent: Integer;
    CutLastLineBreak: Boolean;
    CutPos: Integer;
  
    procedure AddBeautified(const s: string);
    var
      NewStr: String;
      LengthOfLastLine: integer;
      LineEndCnt: Integer;
      CurIndent: Integer;
      FirstLineIndent: Integer;
      EndPos: Integer;
    begin
      if s='' then exit;
      NewStr:=s;
      CurIndent:=OldSelectionIndent;
      if NewSelect.Position=0 then begin
        FirstLineIndent:=OldSelectionIndent-SelectionStart.X+1;
        if FirstLineIndent<0 then FirstLineIndent:=0;
        NewStr:=GetIndentStr(FirstLineIndent)+NewStr;
        dec(CurIndent,FirstLineIndent);
        if CurIndent<0 then CurIndent:=0;
      end;
      //debugln('AddBeautified A X=',X,' Y=',Y,' CurIndent=',CurIndent,' NewStr="',NewStr,'"');
      dec(CurIndent,GetLineIndent(NewStr,1));
      if CurIndent<0 then CurIndent:=0;
      NewStr:=CodeToolBoss.SourceChangeCache.BeautifyCodeOptions.BeautifyStatement(
                  NewStr,CurIndent,
                  [bcfIndentExistingLineBreaks,bcfDoNotIndentFirstLine]);
      LineEndCnt:=LineEndCount(NewStr,LengthOfLastLine);
      if (TemplatePos>TemplateLen) then begin
        // cut indent at end of template
        if LineEndCnt>0 then begin
          EndPos:=length(NewStr);
          while (EndPos>=1) and (NewStr[EndPos]=' ') do dec(EndPos);
          NewStr:=copy(NewStr,1,length(NewStr)-CurIndent);
          LineEndCnt:=LineEndCount(NewStr,LengthOfLastLine);
        end;
      end;
      inc(Y,LineEndCnt);
      if LineEndCnt=0 then
        inc(X,LengthOfLastLine)
      else
        X:=LengthOfLastLine+1;
      if (LineEndCnt>0) or (NewSelect.Position=0) then
        TemplateIndent:=GetLineIndent(NewStr,length(NewStr)+1);
      //debugln('AddBeautified B X=',X,' Y=',Y,' TemplateIndent=',TemplateIndent,' LengthOfLastLine=',LengthOfLastLine,' NewStr="',NewSTr,'"');
      NewSelect.Write(NewStr[1],length(NewStr));
    end;
    
    procedure FlushTemplate;
    var
      FromPos: Integer;
      ToPos: Integer;
    begin
      FromPos:=LastWrittenTemplatePos+1;
      ToPos:=TemplatePos-1;
      if ToPos>TemplateLen then ToPos:=TemplateLen;
      if FromPos<=ToPos then
        AddBeautified(copy(Template,FromPos,ToPos-FromPos+1));
      LastWrittenTemplatePos:=ToPos;
    end;
    
    procedure CalculateCursorPos;
    begin
      NewCursor:=Point(X,Y);
    end;
    
    procedure InsertSelection;
    var
      CurY: Integer;
      CurLine: string;
      IndentStr: String;
      MinX: Integer;
      MaxX: Integer;
      l: Integer;
    begin
      IndentStr:=GetIndentStr(TemplateIndent-OldSelectionIndent);
      for CurY:=SelectionStart.Y to SelectionEnd.Y do begin
        CurLine:=Source[CurY-1];
        //debugln(['InsertSelection CurY=',CurY,' CurLine="',dbgstr(CurLine),'"']);
        MinX:=1;
        MaxX:=length(CurLine)+1;
        if (CurY=SelectionStart.Y) then begin
          MinX:=SelectionStart.X;
          if MinX<=OldSelectionIndent then
            MinX:=OldSelectionIndent+1;
          if MinX>MaxX then
            MinX:=MaxX;
        end;
        if (CurY=SelectionEnd.Y) and (MaxX>SelectionEnd.X) then
          MaxX:=SelectionEnd.X;
        //debugln(['InsertSelection CurY=',CurY,' Range=',MinX,'-',MaxX,' Indent="',length(IndentStr),'" "',copy(CurLine,MinX,MaxX-MinX),'"']);
        X:=1;
        // write indent
        if (IndentStr<>'') and (CurY<>SelectionStart.Y) then begin
          NewSelect.Write(IndentStr[1],length(IndentStr));
          inc(X,length(IndentStr));
        end;
        // write line
        l:=MaxX-MinX;
        if l>0 then begin
          NewSelect.Write(CurLine[MinX],l);
          inc(X,l);
        end;
        // write line break and adjust cursor
        if CurY<SelectionEnd.Y then begin
          NewSelect.Write(EndOfLine[1],length(EndOfLine));
          inc(Y);
          X:=1;
        end;
      end;
    end;
    
    procedure ParseMacro;
    var
      MacroNameStart: Integer;
      MacroNameEnd: Integer;
      
      function MacroNameIs(const Name: string): boolean;
      begin
        Result:=CompareText(@Template[MacroNameStart],MacroNameEnd-MacroNameStart,
                            @Name[1],length(Name),false)=0;
      end;
      
    begin
      FlushTemplate;
      inc(TemplatePos);
      MacroNameStart:=TemplatePos;
      while (TemplatePos<=TemplateLen)
      and (Template[TemplatePos] in ['a'..'z','A'..'Z','_','0'..'9']) do
        inc(TemplatePos);
      MacroNameEnd:=TemplatePos;
      if (TemplatePos<=TemplateLen) and (Template[TemplatePos]='>') then begin
        LastWrittenTemplatePos:=TemplatePos;
        inc(TemplatePos);
        if MacroNameIs('Selection') then begin
          InsertSelection;
        end;
      end;
    end;
    
    procedure GetOldSelectionIndent;
    var
      CurY: Integer;
      CurLine: string;
      CurIndent: Integer;
    begin
      OldSelectionIndent:=0;
      CurY:=SelectionStart.Y;
      while CurY<Source.Count do begin
        CurLine:=Source[CurY-1];
        CurIndent:=GetLineIndent(CurLine,1);
        if CurIndent<length(CurLine) then begin
          OldSelectionIndent:=CurIndent;
          break;
        end;
        inc(CurY);
      end;
    end;
    
  begin
    //debugln(['EncloseTextSelection A ',SelectionStart.X,',',SelectionStart.Y,'-',SelectionEnd.X,',',SelectionEnd.Y,' indent=',Indent,' Template="',Template,'"']);
    NewSelection:='';
    NewCursor:=Point(0,0);
    CutLastLineBreak:=true;
    if (SelectionEnd.X=1) and (SelectionEnd.Y>SelectionStart.Y) then begin
      CutLastLineBreak:=false;
      dec(SelectionEnd.Y);
      if SelectionEnd.Y<Source.Count then
        SelectionEnd.X:=length(Source[SelectionEnd.Y-1])+1;
    end;
    NewSelect:=TMemoryStream.Create;
    NewCursor:=SelectionStart;
    X:=NewCursor.X;
    Y:=NewCursor.Y;
    GetOldSelectionIndent;
    TemplateIndent:=OldSelectionIndent;
    try
      TemplateLen:=length(Template);
      TemplatePos:=1;
      LastWrittenTemplatePos:=TemplatePos-1;
      while TemplatePos<=TemplateLen do begin
        case Template[TemplatePos] of
          '\':
            begin
              FlushTemplate;
              LastWrittenTemplatePos:=TemplatePos;
              inc(TemplatePos,2);
            end;
            
          '|':
            begin
              FlushTemplate;
              CalculateCursorPos;
              LastWrittenTemplatePos:=TemplatePos;
              inc(TemplatePos);
            end;
            
          '<':
            ParseMacro;
  
        else
          inc(TemplatePos);
        end;
      end;
      FlushTemplate;
    finally
      SetLength(NewSelection,NewSelect.Size);
      if NewSelection<>'' then begin
        NewSelect.Position:=0;
        NewSelect.Read(NewSelection[1],length(NewSelection));
        //debugln(['EncloseTextSelection CutLastLineBreak=',CutLastLineBreak,' NewSelection="',NewSelection,'"']);
        if CutLastLineBreak then begin
          CutPos:=length(NewSelection);
          if NewSelection[CutPos] in [#10,#13] then begin
            dec(CutPos);
            if (CutPos>=1) and (NewSelection[CutPos] in [#10,#13])
            and (NewSelection[CutPos]<>NewSelection[CutPos+1]) then begin
              dec(CutPos);
            end;
            NewSelection:=copy(NewSelection,1,CutPos);
          end;
        end;
      end;
      NewSelect.Free;
    end;
  end;
  
  { TEncloseSelectionDialog }
  
  procedure TEncloseSelectionDialog.EncloseSelectionDialogCREATE(Sender: TObject);
  var
    t: TEncloseSelectionType;
  begin
    Caption:=lisKMEncloseSelection;
  
    TypeRadiogroup.Caption:=lisChooseStructureToEncloseSelection;
    with TypeRadiogroup.Items do begin
      BeginUpdate;
      for t:=Low(TEncloseSelectionType) to High(TEncloseSelectionType) do
        Add(EncloseSelectionTypeDescription(t));
      EndUpdate;
    end;
    TypeRadiogroup.ItemIndex:=0;
  end;
  
  function TEncloseSelectionDialog.GetEncloseType: TEncloseSelectionType;
  var
    i: Integer;
  begin
    i:=TypeRadiogroup.ItemIndex;
    for Result:=Low(TEncloseSelectionType) to High(TEncloseSelectionType) do
      if UTF8CompareText(TypeRadiogroup.Items[i],
                         EncloseSelectionTypeDescription(Result))=0
      then
        exit;
    RaiseGDBException('TEncloseSelectionDialog.GetEncloseType');
  end;
  
  end.
  
  