unit ce_staticmacro;

{$I ce_defines.inc}

interface

uses
  Classes, Sysutils, SynEdit, SynCompletion, RegExpr,
  ce_common, ce_interfaces, ce_writableComponent, ce_synmemo;

type


  (**
   * Static macros options containers
   *)
  TStaticMacrosOptions = class(TWritableLfmTextComponent)
  private
    fAutoInsert: boolean;
    fShortCut: TShortCut;
    fMacros: TStringList;
    fSetDef: TCEEditEvent;
    procedure setMacros(value: TStringList);
    procedure setDefEvent(value: TCEEditEvent);
  published
    property autoInsert: boolean read fAutoInsert write fAutoInsert;
    property macros: TStringList read fMacros write setMacros;
    property shortcut: TShortCut read fShortCut write fShortCut;
    property resetDefault: TCEEditEvent read fSetDef write setDefEvent stored false;
	public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure AssignTo(target: TPersistent); override;
  end;

  (**
   * TCEStaticEditorMacro is used to insert static macros (parameter-less code snippets)
   * in an editor. A macro begins with the dollar symbol and ends with an alphanum.
   *
   * The dollar symbol is used as starter because it's usually accessible without
   * modifier: no CTRL, no ALT, no SHIFT.
   * Erroneous insertion is avoided because in D '$' is either followed
   * by a symbol: '$-1', '$]' or by a blank '$ ]'
   *
   * Shift + SPACE works automatically on the right editor (ICEDocumentObserver)
   * Automatic insertion is handled in TCESynMemo.KeyUp()
   *)
  TCEStaticEditorMacro = class(TWritableLfmTextComponent, ICEDocumentObserver, ICEEditableOptions, ICEEditableShortCut)
  private
    fValidator: TRegExpr;
    fCompletor: TSynAutoComplete;
    fMacros: TStringList;
    fDoc: TCESynMemo;
    fAutomatic: boolean;
    fOptions: TStaticMacrosOptions;
    fOptionBackup: TStaticMacrosOptions;
    procedure sanitize;
    procedure addDefaults;
    procedure updateCompletor;
    procedure setMacros(value: TStringList);
    // ICEDocumentObserver
    procedure docNew(document: TCESynMemo);
    procedure docFocused(document: TCESynMemo);
    procedure docChanged(document: TCESynMemo);
    procedure docClosing(document: TCESynMemo);
    // ICEEditableOptions
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
    // ICEEditableShortcut
    function scedWantFirst: boolean;
    function scedWantNext(out category, identifier: string; out aShortcut: TShortcut): boolean;
    procedure scedSendItem(const category, identifier: string; aShortcut: TShortcut);
    procedure scedSendDone;
  published
    // list of string with the format $<..>alnum=<..>
    property macros: TStringList read fMacros write setMacros;
    property automatic: boolean read fAutomatic write fAutomatic;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    // execute using the editor
    procedure Execute; overload;
    // execute in aEditor, according to aToken
    procedure Execute(aEditor: TCustomSynEdit; const aToken: string); overload;
  end;

var
  StaticEditorMacro: TCEStaticEditorMacro = nil;

implementation

uses
  ce_observer;

const
  OptFname = 'staticmacros.txt';

  defMacros: array[0..15] of string = (
    '$a=auto',
    '$b={}',
    '$c=class {}',
    '$e=enum {}',
    '$it=interface {}',
    '$im=import ',
    '$pr=protected {}',
    '$pu=public {}',
    '$pv=private {}',
    '$s=struct {}',
    '$t=template {}',
    '$un=union{}',
    '$ut=unittest{}',
    '$fo=for(auto i = 0; ; )',
    '$fe=foreach(elem; )',
    '$v=void (){}'
  );

{$REGION TStaticMacrosOptions --------------------------------------------------}

constructor TStaticMacrosOptions.create(aOwner: TComponent);
begin
	inherited;
  fMacros := TStringList.Create;
end;

destructor TStaticMacrosOptions.destroy;
begin
  fMacros.Free;
  inherited;
end;

procedure TStaticMacrosOptions.Assign(Source: TPersistent);
var
  edmac: TCEStaticEditorMacro;
  opt: TStaticMacrosOptions;
begin
	if Source is TCEStaticEditorMacro then
  begin
    edmac := TCEStaticEditorMacro(Source);
    //
    fAutoInsert := edmac.automatic;
    fMacros.Assign(edmac.fMacros);
    fShortCut := edmac.fCompletor.ShortCut;
  end
  else if Source is  TStaticMacrosOptions then
  begin
    opt := TStaticMacrosOptions(Source);
    //
    autoInsert := opt.autoInsert;
    macros.Assign(opt.fMacros);
    shortcut := opt.shortcut;
  end
  else inherited;
end;

procedure TStaticMacrosOptions.AssignTo(target: TPersistent);
var
  edmac: TCEStaticEditorMacro;
  opt: TStaticMacrosOptions;
begin
	if target is TCEStaticEditorMacro then
  begin
    edmac := TCEStaticEditorMacro(target);
    //
    edmac.automatic := fAutoInsert;
    // setMacros sanitizes the macros
    edmac.setMacros(fMacros);
    fMacros.Assign(edmac.fMacros);
    //
    edmac.fCompletor.ShortCut := fShortCut;
  end
  else if target is  TStaticMacrosOptions then
  begin
    opt := TStaticMacrosOptions(target);
    //
    opt.autoInsert := autoInsert;
    opt.macros.Assign(fMacros);
    opt.shortcut := shortcut;
  end
  else inherited;
end;

procedure TStaticMacrosOptions.setMacros(value: TStringList);
begin
  fMacros.Assign(value);
end;

procedure TStaticMacrosOptions.setDefEvent(value : TCEEditEvent);
begin
  TCEStaticEditorMacro(owner).addDefaults;
  fMacros.Assign(TCEStaticEditorMacro(owner).fMacros);
end;
{$ENDREGION}

{$REGION Standard Comp/Obj -----------------------------------------------------}
constructor TCEStaticEditorMacro.create(aOwner: TComponent);
var
  fname: string;
begin
  inherited;
  fAutomatic := true;
  fValidator := TRegExpr.Create('^\$\w*[a-zA-Z]$');
  fValidator.Compile;
  fCompletor := TSynAutoComplete.Create(self);
  fCompletor.ShortCut := 8224; // SHIFT + SPACE
  fCompletor.EndOfTokenChr:= '';
  fMacros := TStringList.Create;
  fMacros.Delimiter := '=';
  //
  fOptions := TStaticMacrosOptions.create(self);
  fOptionBackup := TStaticMacrosOptions.create(self);
  fname := getCoeditDocPath + OptFname;
  if fname.fileExists then
  begin
    fOptions.loadFromFile(fname);
    // old option file will create a streaming error.
    if fOptions.hasLoaded then
    	fOptions.AssignTo(self)
    else
    	fOptions.Assign(self);
  end
  else
  begin
    addDefaults;
    fOptions.Assign(self);
  end;
  //
  sanitize;
  updateCompletor;
  //
  EntitiesConnector.addObserver(Self);
end;

destructor TCEStaticEditorMacro.destroy;
begin
  fOptions.saveToFile(getCoeditDocPath + OptFname);
  EntitiesConnector.removeObserver(Self);
  //
  fMacros.Free;
  fValidator.Free;
  inherited;
end;

procedure TCEStaticEditorMacro.setMacros(value: TStringList);
begin
  fMacros.Assign(value);
  sanitize;
  updateCompletor;
end;
{$ENDREGION}

{$REGION ICEDocumentObserver ---------------------------------------------------}
procedure TCEStaticEditorMacro.docNew(document: TCESynMemo);
begin
  fDoc := document;
  fCompletor.Editor := fDoc;
end;

procedure TCEStaticEditorMacro.docFocused(document: TCESynMemo);
begin
  if fDoc = document then exit;
  fDoc := document;
  fCompletor.Editor := fDoc;
end;

procedure TCEStaticEditorMacro.docChanged(document: TCESynMemo);
begin
  if document <> fDoc then
    exit;
end;

procedure TCEStaticEditorMacro.docClosing(document: TCESynMemo);
begin
  if document <> fDoc then
    exit;
  fDoc := nil;
end;
{$ENDREGION}

{$REGION ICEEditableOptions ----------------------------------------------------}
function TCEStaticEditorMacro.optionedWantCategory(): string;
begin
	exit('Static macros');
end;

function TCEStaticEditorMacro.optionedWantEditorKind: TOptionEditorKind;
begin
	exit(oekGeneric);
end;

function TCEStaticEditorMacro.optionedWantContainer: TPersistent;
begin
	fOptions.Assign(self);
  fOptionBackup.Assign(fOptions);
  exit(fOptions);
end;

procedure TCEStaticEditorMacro.optionedEvent(event: TOptionEditorEvent);
begin
	case event of
    oeeAccept:
    begin
      fOptions.AssignTo(self);
      fOptionBackup.Assign(self);
    end;
    oeeCancel:
    begin
      fOptionBackup.AssignTo(self);
      fOptionBackup.AssignTo(fOptions);
    end;
    oeeChange: fOptions.AssignTo(self);
  end;
end;

function TCEStaticEditorMacro.optionedOptionsModified: boolean;
begin
  exit(false);
end;
{$ENDREGION}

{$REGION ICEEditableShortCut ---------------------------------------------------}
function TCEStaticEditorMacro.scedWantFirst: boolean;
begin
  exit(true);
end;

function TCEStaticEditorMacro.scedWantNext(out category, identifier: string; out aShortcut: TShortcut): boolean;
begin
  category := 'Static macros';
  identifier := 'invoke';
  aShortcut := fCompletor.ShortCut;
  exit(false);
end;

procedure TCEStaticEditorMacro.scedSendItem(const category, identifier: string; aShortcut: TShortcut);
begin
  if category = 'Static macros' then
    if identifier = 'invoke' then begin
      fCompletor.ShortCut := aShortcut;
      fOptionBackup.shortcut := aShortcut;
      fOptions.shortcut := aShortcut;
    end;
end;

procedure TCEStaticEditorMacro.scedSendDone;
begin
end;
{$ENDREGION}

{$REGION Macros things ---------------------------------------------------------}
procedure TCEStaticEditorMacro.sanitize;
var
  i: Integer;
begin
  for i := fMacros.Count-1 downto 0 do
    if not fValidator.Exec(fMacros.Names[i]) then
      fMacros.Delete(i);
end;

procedure TCEStaticEditorMacro.addDefaults;
var
  i: Integer;
begin
  for i := 0 to high(defMacros) do
    if fMacros.IndexOf(defMacros[i]) = -1 then
      fMacros.Add(defMacros[i]);
end;

procedure TCEStaticEditorMacro.updateCompletor;
var
  i: Integer;
  tok, val: string;
begin
  fCompletor.AutoCompleteList.Clear;
  for i := 0 to fMacros.Count-1 do
  begin
    tok := fMacros.Names[i];
    val := fMacros.ValueFromIndex[i];
    fCompletor.AutoCompleteList.Add(tok);
    fCompletor.AutoCompleteList.Add('=' + val);
  end;
end;

procedure TCEStaticEditorMacro.Execute;
begin
  if fDoc.isNotNil then
    fCompletor.Execute(fDoc.Identifier, fDoc);
end;

procedure TCEStaticEditorMacro.Execute(aEditor: TCustomSynEdit; const aToken: string);
begin
  if aEditor.isNotNil then
    fCompletor.Execute(aToken, aEditor);
end;
{$ENDREGION}

initialization
  StaticEditorMacro := TCEStaticEditorMacro.create(nil);
finalization
  StaticEditorMacro.Free;;
end.

