unit SynTextMateSyn;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, fgl,
  jsonparser, jsonscanner, fpjson,
  // LazUtils
  LazFileUtils,
  Laz2_XMLRead, Laz2_DOM, LazStringUtils,
  // LazEdit
  TextMateGrammar,
  // SynEdit
  SynEditHighlighter, SynEditHighlighterFoldBase, SynEditTypes, SynEditTextBase;

type

  TNameAttributesMap   = specialize TFPGMapObject<string, TSynHighlighterAttributes>;
  TGrammarLoadEvent = procedure(AGrammarFile, AGrammarPath: String; out AGrammarDef: String);

 TSynTextMateRangeInfo = record
    FoldLevel: Smallint;
  end;

  { TSynHighlighterTextMateRangeList }

  TSynHighlighterTextMateRangeList = class(TSynHighlighterRangeList)
  private
    FItemOffset: integer;
    function GetRangeInfo(Index: Integer): TSynTextMateRangeInfo;
    procedure SetRangeInfo(Index: Integer; AValue: TSynTextMateRangeInfo);
  public
    constructor Create;
    property RangeInfo[Index: Integer]: TSynTextMateRangeInfo read GetRangeInfo write SetRangeInfo;
  end;

  { TSynTextMateSyn }

  TSynTextMateSyn = class(TSynCustomFoldHighlighter)
  private
    FGrammarPath: String;
    FOnLoadGrammarFile: TGrammarLoadEvent;
    FTextMateGrammar: TTextMateGrammar;

  private
    FAttriMap: TNameAttributesMap;

    function  LoadFile(AGrammarFile: String): String;
    procedure SetGrammarPath(AValue: String);

    function GetOrCreateAttribIdxForName(AName: String): integer;
    procedure DoPopulateAttributeInfo(Sender: TTextMateGrammar; APattern: TTextMatePattern;
      AContextName: String; var AnAttribInfo: TSynAttributeInfo);
    procedure DoCheckAttributeInfo(Sender: TTextMatePattern;
      const AnAttribInfo: TSynAttributeInfo; out AnUseId, AnUseObject: Boolean);
  private
    FCurrentRange: Integer;
    FRangeInfo: TSynTextMateRangeInfo;
    FCurrentTokenPos, FCurrentTokenLen: Integer;
    FCurrentTokenKind: integer;
    FCurrentAttrib: TSynHighlighterAttributes;
    function GetParserError: String;

  protected
    function GetInstanceLanguageName: string; override;
    function CreateRangeList(ALines: TSynEditStringsBase): TSynHighlighterRangeList; override;
    function UpdateRangeInfoAtLine(Index: Integer): Boolean; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure LoadGrammar(AGrammarDef: String);
    procedure LoadGrammar(AGrammarFile, AGrammarPath: String);

    procedure SetLine(const NewValue: String; LineNumber: Integer); override;
    procedure Next; override;
    function GetEol: Boolean; override;
    function DebugAttrAtXPos(AXPos: Integer): String; // requires StartAtLineIndex before

    function GetToken: String; override;
    procedure GetTokenEx(out TokenStart: PChar; out TokenLength: integer); override;
    function GetTokenPos: Integer; override;
    function GetTokenKind: integer; override;
    function GetTokenAttribute: TSynHighlighterAttributes; override;
    //
    function GetDefaultAttribute(Index: integer): TSynHighlighterAttributes; override;

    procedure SetRange(Value: Pointer); override;
    procedure ResetRange; override;
    function GetRange: Pointer; override;

    function FoldBlockEndLevel(ALineIndex: TLineIdx; const AFilter: TSynFoldBlockFilter): integer; override; overload;
    function FoldBlockMinLevel(ALineIndex: TLineIdx; const AFilter: TSynFoldBlockFilter): integer; override; overload;

    property OnLoadGrammarFile: TGrammarLoadEvent read FOnLoadGrammarFile write FOnLoadGrammarFile;
    property GrammarPath: String read FGrammarPath write SetGrammarPath;
    property ParserError: String read GetParserError;
    property TextMateGrammar: TTextMateGrammar read FTextMateGrammar;
  end;

implementation

{ TSynHighlighterTextMateRangeList }

function TSynHighlighterTextMateRangeList.GetRangeInfo(Index: Integer
  ): TSynTextMateRangeInfo;
begin
  if (Index < 0) or (Index >= Count) then
    Result := Default(TSynTextMateRangeInfo)
  else
    Result := TSynTextMateRangeInfo((ItemPointer[Index] + FItemOffset)^);
end;

procedure TSynHighlighterTextMateRangeList.SetRangeInfo(Index: Integer;
  AValue: TSynTextMateRangeInfo);
begin
  TSynTextMateRangeInfo((ItemPointer[Index] + FItemOffset)^) := AValue;
end;

constructor TSynHighlighterTextMateRangeList.Create;
begin
  inherited;
  FItemOffset := ItemSize;
  ItemSize := FItemOffset + SizeOf(TSynTextMateRangeInfo);
end;

{ TSynTextMateSyn }

function TSynTextMateSyn.LoadFile(AGrammarFile: String): String;
var
  s: TStringStream;
begin
  if Assigned(FOnLoadGrammarFile) then begin
    OnLoadGrammarFile(AGrammarFile, FGrammarPath, Result);
  end
  else begin
    s := TStringStream.Create('');
    try
      s.LoadFromFile(FGrammarPath + AGrammarFile);
      Result := s.DataString;
    finally
      s.Free;
    end;
  end;
end;

procedure TSynTextMateSyn.SetGrammarPath(AValue: String);
begin
  if FGrammarPath = AValue then Exit;
  if AValue <> '' then
    AppendPathDelim(AValue);
  FGrammarPath := AValue;
end;

function TSynTextMateSyn.GetOrCreateAttribIdxForName(AName: String): integer;
var
  attr: TSynHighlighterAttributes;
begin
  if AName = '' then
    exit(-1);
  Result := FAttriMap.IndexOf(AName);
  if Result >= 0 then
    exit;
  attr := TSynHighlighterAttributes.Create(AName, AName);
  AddAttribute(attr);
  Result := FAttriMap.Add(AName, attr);
end;

function TSynTextMateSyn.GetInstanceLanguageName: string;
begin
  Result := FTextMateGrammar.LanguageName;
end;

function TSynTextMateSyn.CreateRangeList(ALines: TSynEditStringsBase
  ): TSynHighlighterRangeList;
begin
  Result := TSynHighlighterTextMateRangeList.Create;
end;

function TSynTextMateSyn.UpdateRangeInfoAtLine(Index: Integer): Boolean;
var
  r: TSynTextMateRangeInfo;
  i: Integer;
begin
  GetRange;
  i := {%H-}Integer(TSynHighlighterTextMateRangeList(CurrentRanges).Range[Index]);
  if i <> FCurrentRange then
    FTextMateGrammar.MainPatternList[i].DecRefCount;

  Result := inherited;
  r := TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[Index];
  Result := Result
        or (FRangeInfo.FoldLevel <> r.FoldLevel);
  TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[Index] := FRangeInfo;

  if i <> FCurrentRange then
    FTextMateGrammar.MainPatternList[Integer(i)].IncRefCount;
end;

procedure TSynTextMateSyn.DoPopulateAttributeInfo(
  Sender: TTextMateGrammar; APattern: TTextMatePattern;
  AContextName: String; var AnAttribInfo: TSynAttributeInfo);
begin
  AnAttribInfo.TokId := GetOrCreateAttribIdxForName(AContextName);
  if AnAttribInfo.TokId < 0 then
    AnAttribInfo.TokObject := nil
  else
    AnAttribInfo.TokObject := FAttriMap.Data[AnAttribInfo.TokId];
end;

procedure TSynTextMateSyn.DoCheckAttributeInfo(Sender: TTextMatePattern;
  const AnAttribInfo: TSynAttributeInfo; out AnUseId, AnUseObject: Boolean);
begin
  AnUseId := AnAttribInfo.TokId >= 0;
  AnUseObject := (AnAttribInfo.TokObject <> nil) and
                 (TSynHighlighterAttributes(AnAttribInfo.TokObject).IsEnabled);
end;

function TSynTextMateSyn.GetParserError: String;
begin
  Result := FTextMateGrammar.ParserError;
end;

constructor TSynTextMateSyn.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAttriMap := TNameAttributesMap.Create(False);
  FTextMateGrammar := TTextMateGrammar.Create;
  FTextMateGrammar.OnPopulateAttributeInfo := @DoPopulateAttributeInfo;
  FTextMateGrammar.OnCheckAttributeInfo := @DoCheckAttributeInfo;
end;

destructor TSynTextMateSyn.Destroy;
begin
  FTextMateGrammar.ClearGrammar;
  FTextMateGrammar.Free;
  FreeHighlighterAttributes;
  FAttriMap.Clear;
  FAttriMap.Free;
  inherited Destroy;
end;

procedure TSynTextMateSyn.LoadGrammar(AGrammarDef: String);
begin
  FTextMateGrammar.ParseGrammar(AGrammarDef);
end;

procedure TSynTextMateSyn.LoadGrammar(AGrammarFile, AGrammarPath: String);
begin
  GrammarPath := AGrammarPath;
  FTextMateGrammar.ParseGrammar(LoadFile(AGrammarFile));
  if FTextMateGrammar.LanguageName = '' then
    FTextMateGrammar.LanguageName := AGrammarFile;
end;

procedure TSynTextMateSyn.SetLine(const NewValue: String;
  LineNumber: Integer);
var
  nd: TSynFoldNodeInfo;
begin
  inherited SetLine(NewValue, LineNumber);

  if FCurrentRange = -2 then
    FCurrentRange := FTextMateGrammar.CurrentPatternIndex;
// TODO setline - keep range?

  FTextMateGrammar.SetLine(CurrentLineText, FCurrentRange);
  FCurrentRange := -2;

  if FTextMateGrammar.IsFoldBegin then begin
    if not FTextMateGrammar.IsFoldEnd then begin
      inc(FRangeInfo.FoldLevel);
      if IsCollectingNodeInfo then begin
        nd := Default(TSynFoldNodeInfo);
        nd.LineIndex := LineIndex;
        nd.FoldGroup := 1;
        nd.FoldLvlStart := FRangeInfo.FoldLevel - 1;
        nd.FoldLvlEnd := FRangeInfo.FoldLevel;
        nd.NestLvlStart := FRangeInfo.FoldLevel - 1;
        nd.NestLvlEnd := FRangeInfo.FoldLevel;
        nd.FoldAction := [sfaFold, sfaFoldFold, sfaOpen, sfaOpenFold];
        CollectingNodeInfoList.Add(nd);
      end;
    end;
  end
  else
  if FTextMateGrammar.IsFoldEnd and (FRangeInfo.FoldLevel > 0) then begin
    dec(FRangeInfo.FoldLevel);
    if IsCollectingNodeInfo then begin
      nd := Default(TSynFoldNodeInfo);
      nd.LineIndex := LineIndex;
      nd.FoldGroup := 1;
      nd.FoldLvlStart := FRangeInfo.FoldLevel + 1;
      nd.FoldLvlEnd := FRangeInfo.FoldLevel;
      nd.NestLvlStart := FRangeInfo.FoldLevel + 1;
      nd.NestLvlEnd := FRangeInfo.FoldLevel;
      nd.FoldAction := [sfaFold, sfaFoldFold, sfaClose, sfaCloseFold];
      CollectingNodeInfoList.Add(nd);
    end;
  end;

  if IsScanning then begin
    FTextMateGrammar.NextToEol;
  end
  else begin
    FTextMateGrammar.First;
    FCurrentTokenKind := FTextMateGrammar.CurrentTokenKind;
    FCurrentAttrib    := TSynHighlighterAttributes(FTextMateGrammar.CurrentAttrib);
    FCurrentTokenPos  := FTextMateGrammar.CurrentTokenPos;
    FCurrentTokenLen  := FTextMateGrammar.CurrentTokenLen;
  end;
  //FCurrentRange := FTextMateGrammar.CurrentPatternIndex;

end;

procedure TSynTextMateSyn.Next;
begin
  FTextMateGrammar.Next;
  FCurrentTokenKind := FTextMateGrammar.CurrentTokenKind;
  FCurrentAttrib    := TSynHighlighterAttributes(FTextMateGrammar.CurrentAttrib);
  FCurrentTokenPos  := FTextMateGrammar.CurrentTokenPos;
  FCurrentTokenLen  := FTextMateGrammar.CurrentTokenLen;
  //FCurrentRange     := FTextMateGrammar.CurrentPatternIndex;
end;

function TSynTextMateSyn.DebugAttrAtXPos(AXPos: Integer): String;
var
  st: TTextMatePatternState;
  i: Integer;
begin
  Result := '';
  st := FTextMateGrammar.CurrentState;
  SetLength(st.StateList, Length(st.StateList)) ;
  while (FCurrentTokenPos + FCurrentTokenLen < AXPos) do begin
    if GetEol then begin
      result := format('EOL At %d Len %d %s', [FCurrentTokenPos, FCurrentTokenLen, LineEnding]);
      exit;
    end;
    st := FTextMateGrammar.CurrentState;
    SetLength(st.StateList, Length(st.StateList)) ;
    Next;
  end;

  result := format('At %d Len %d %s', [FCurrentTokenPos, FCurrentTokenLen, LineEnding]);

  for i := st.StateIdx downto 0 do begin
    if st.StateList[i].Pattern <> nil then begin
      if (st.StateList[i].Pattern is TTextMatePatternBaseNested) and
         (st.StateList[i].Pattern.ForwardTarget <> nil)
      then
        Result := Result + Format('%2d: %4d: %s / %s >// %s %s%s', [
          i,
          st.StateList[i].Pattern.ForwardTarget.Index,
          st.StateList[i].Pattern.ForwardTarget.Name,
          st.StateList[i].Pattern.ForwardTarget.Comment,
          StripLN(st.StateList[i].Pattern.DebugDump(0,false,'')),
          st.StateList[i].Pattern.ForwardTarget.ClassName,
          LineEnding
        ])
      else
        Result := Result + Format('%2d: %4d: %s / %s  // %s%s%s', [
          i,
          st.StateList[i].Pattern.Index,
          st.StateList[i].Pattern.Name,
          st.StateList[i].Pattern.Comment,
          StripLN(st.StateList[i].Pattern.DebugDump(0,false,'')),
          st.StateList[i].Pattern.ClassName,
          LineEnding
        ]);
      end
      else
        Result := Result + '???';
  end;
end;

function TSynTextMateSyn.GetEol: Boolean;
begin
  Result := FTextMateGrammar.IsAtEol;
end;

function TSynTextMateSyn.GetToken: String;
begin
  Result := Copy(CurrentLineText, FCurrentTokenPos, FCurrentTokenLen);
end;

procedure TSynTextMateSyn.GetTokenEx(out TokenStart: PChar; out
  TokenLength: integer);
begin
  TokenStart := @CurrentLineText[FCurrentTokenPos];
  TokenLength := FCurrentTokenLen;
end;

function TSynTextMateSyn.GetTokenPos: Integer;
begin
  Result := FCurrentTokenPos - 1;
end;

function TSynTextMateSyn.GetTokenKind: integer;
begin
  Result := FCurrentTokenKind;
end;

function TSynTextMateSyn.GetTokenAttribute: TSynHighlighterAttributes;
begin
  Result := FCurrentAttrib;
end;

function TSynTextMateSyn.GetDefaultAttribute(Index: integer
  ): TSynHighlighterAttributes;
begin
  Result := FAttriMap.Data[FTextMateGrammar.RootPattern.AttribInfo.TokId];
end;

procedure TSynTextMateSyn.SetRange(Value: Pointer);
begin
  FCurrentRange := PtrUInt(Value);
  FRangeInfo := TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[LineIndex-1];
end;

procedure TSynTextMateSyn.ResetRange;
begin
  FCurrentRange := -1;
  FRangeInfo := Default(TSynTextMateRangeInfo);
end;

function TSynTextMateSyn.GetRange: Pointer;
begin
  FCurrentRange     := FTextMateGrammar.CurrentPatternIndex;
  Result := Pointer(PtrUInt(FCurrentRange));
end;

function TSynTextMateSyn.FoldBlockEndLevel(ALineIndex: TLineIdx;
  const AFilter: TSynFoldBlockFilter): integer;
var
  RangeInfo: TSynTextMateRangeInfo;
begin
  RangeInfo := TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[ALineIndex];
  Result := RangeInfo.FoldLevel;
end;

function TSynTextMateSyn.FoldBlockMinLevel(ALineIndex: TLineIdx;
  const AFilter: TSynFoldBlockFilter): integer;
var
  RangeInfo: TSynTextMateRangeInfo;
begin
  RangeInfo := TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[ALineIndex-1];
  Result := RangeInfo.FoldLevel;
  RangeInfo := TSynHighlighterTextMateRangeList(CurrentRanges).RangeInfo[ALineIndex];
  if Result > RangeInfo.FoldLevel then
    Result := RangeInfo.FoldLevel;
end;

end.

