unit SynGutterLineNumber;

{$I synedit.inc}

interface

uses
  Classes, SysUtils, Graphics, LCLType, LCLIntf, SynGutterBase,
  SynEditMiscProcs, SynTextDrawer, SynEditMouseCmds,
  LazSynEditText, SynEditTypes;

type

  TSynGutterLineNumber = class;

  TSynEditGutterLineInfo = record
    ShowDot: boolean;
    ViewLine: Integer;
    LineRange: TLineRange;
  end;
  TSynEditGetGutterLineTextEvent = procedure(Sender: TSynGutterLineNumber; ALine: integer; out AText: string; const ALineInfo: TSynEditGutterLineInfo) of object;

  TSynEditMouseActionsLineNum = class(TSynEditMouseInternalActions)
  public  // empty by default
  end;

  { TSynGutterLineNumber }

  TSynGutterLineNumber = class(TSynGutterPartBase)
  private
    FTextDrawer: TheTextDrawer;

    FDigitCount: integer;
    FAutoSizeDigitCount: integer;
    FShowOnlyLineNumbersMultiplesOf: integer;
    FLeadingZeros: boolean;
    FZeroStart: boolean;
    FOnFormatLineNumber: TSynEditGetGutterLineTextEvent;

    procedure SetDigitCount(AValue : integer);
    procedure SetLeadingZeros(const AValue : boolean);
    procedure SetShowOnlyLineNumbersMultiplesOf(const AValue : integer);
    procedure SetZeroStart(const AValue : boolean);
  protected
    procedure Init; override;
    function  PreferedWidthAtCurrentPPI: Integer; override;
    procedure LineCountChanged(Sender: TSynEditStrings; AIndex, ACount: Integer);
    procedure BufferChanged(Sender: TObject);
    procedure FontChanged(Sender: TObject);
    procedure SetAutoSize(const AValue : boolean); override;
    procedure SetVisible(const AValue: boolean); override;
    function CreateMouseActions: TSynEditMouseInternalActions; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    function FormatLineNumber(Line: integer; IsDot: boolean): string;
    procedure Paint(Canvas: TCanvas; AClip: TRect; FirstLine, LastLine: integer);
      override;
  published
    property MarkupInfo;
    property MarkupInfoCurrentLine;
    property DigitCount: integer read FDigitCount write SetDigitCount;
    property ShowOnlyLineNumbersMultiplesOf: integer
             read FShowOnlyLineNumbersMultiplesOf write SetShowOnlyLineNumbersMultiplesOf;
    property ZeroStart: boolean read FZeroStart write SetZeroStart;
    property LeadingZeros: boolean read FLeadingZeros write SetLeadingZeros;
    property OnFormatLineNumber: TSynEditGetGutterLineTextEvent read FOnFormatLineNumber write FOnFormatLineNumber;
  end;

implementation

{ TSynGutterLineNumber }

constructor TSynGutterLineNumber.Create(AOwner: TComponent);
begin
  FDigitCount := 2;
  FAutoSizeDigitCount := FDigitCount;
  FShowOnlyLineNumbersMultiplesOf := 1;
  FLeadingZeros := false;
  FZeroStart := False;
  inherited Create(AOwner);
end;

procedure TSynGutterLineNumber.Init;
begin
  inherited Init;
  FTextDrawer := Gutter.TextDrawer;
  ViewedTextBuffer.AddChangeHandler(senrLineCount, @LineCountChanged);
  ViewedTextBuffer.AddNotifyHandler(senrTextBufferChanged, @BufferChanged);
  FTextDrawer.RegisterOnFontChangeHandler(@FontChanged);
  LineCountchanged(nil, 0, 0);
end;

destructor TSynGutterLineNumber.Destroy;
begin
  ViewedTextBuffer.RemoveHandlers(self);
  FTextDrawer.UnRegisterOnFontChangeHandler(@FontChanged);
  inherited Destroy;
end;

procedure TSynGutterLineNumber.Assign(Source : TPersistent);
var
  Src: TSynGutterLineNumber;
begin
  if Source is TSynGutterLineNumber then
  begin
    Src := TSynGutterLineNumber(Source);
    FLeadingZeros := Src.FLeadingZeros;
    FZeroStart := Src.FZeroStart;
    FDigitCount := Src.FDigitCount;
    FAutoSizeDigitCount := Src.FAutoSizeDigitCount;
    FShowOnlyLineNumbersMultiplesOf := Src.FShowOnlyLineNumbersMultiplesOf;
  end;
  inherited;
end;

procedure TSynGutterLineNumber.SetDigitCount(AValue : integer);
begin
  AValue := MinMax(AValue, 2, 99);
  if FDigitCount <> AValue then
  begin
    FDigitCount := AValue;
    if AutoSize then begin
      FAutoSizeDigitCount := Max(FDigitCount, FAutoSizeDigitCount);
      DoAutoSize;
    end else
      FAutoSizeDigitCount := FDigitCount;
    DoChange(Self);
  end;
end;

procedure TSynGutterLineNumber.SetLeadingZeros(const AValue : boolean);
begin
  if FLeadingZeros <> AValue then
  begin
    FLeadingZeros := AValue;
    DoChange(Self);
  end;
end;

procedure TSynGutterLineNumber.SetShowOnlyLineNumbersMultiplesOf(const AValue : integer);
begin
  If FShowOnlyLineNumbersMultiplesOf <> AValue then
  begin
    FShowOnlyLineNumbersMultiplesOf := Max(AValue, 1);
    DoChange(self);
  end;
end;

procedure TSynGutterLineNumber.SetZeroStart(const AValue : boolean);
begin
  if FZeroStart <> AValue then
  begin
    FZeroStart := AValue;
    if AutoSize then
      DoAutoSize;
    DoChange(Self);
  end;
end;

function TSynGutterLineNumber.FormatLineNumber(Line: integer; IsDot: boolean): string;
var
  i: integer;
begin
  Result := '';
  // if a dot must be showed
  if IsDot then
    if Line mod 5 = 0 then // every 5 lines show '-' instead of '.'
      Result := StringOfChar(' ', FAutoSizeDigitCount-1) + '-'
    else
      Result := StringOfChar(' ', FAutoSizeDigitCount-1) + '.'
  // else format the line number
  else begin
    if FZeroStart then Dec(Line);
    Str(Line : FAutoSizeDigitCount, Result);
    if FLeadingZeros then
      for i := 1 to FAutoSizeDigitCount - 1 do begin
        if (Result[i] <> ' ') then break;
        Result[i] := '0';
      end;
  end;
end;

function TSynGutterLineNumber.PreferedWidthAtCurrentPPI: Integer;
begin
  Result := FAutoSizeDigitCount * FTextDrawer.CharWidth + 1;
end;

procedure TSynGutterLineNumber.LineCountChanged(Sender: TSynEditStrings; AIndex, ACount: Integer);
var
  LineCnt, nDigits: integer;
begin
  if not(Visible and AutoSize) then exit;

  LineCnt := TextBuffer.Count;
  if FZeroStart and (LineCnt > 0) then Dec(LineCnt);

  nDigits := Max(Length(IntToStr(LineCnt)), FDigitCount);
  if FAutoSizeDigitCount <> nDigits then begin
    FAutoSizeDigitCount := nDigits;
    DoAutoSize;
  end;
end;

procedure TSynGutterLineNumber.BufferChanged(Sender: TObject);
begin
  LineCountChanged(nil, 0, 0);
end;

procedure TSynGutterLineNumber.FontChanged(Sender: TObject);
begin
  DoAutoSize;
end;

procedure TSynGutterLineNumber.SetAutoSize(const AValue: boolean);
begin
  inherited SetAutoSize(AValue);
  if AValue then LineCountChanged(nil, 0, 0);
end;

procedure TSynGutterLineNumber.SetVisible(const AValue: boolean);
begin
  inherited SetVisible(AValue);
  if AValue then LineCountChanged(nil, 0, 0);
end;

function TSynGutterLineNumber.CreateMouseActions: TSynEditMouseInternalActions;
begin
  Result := TSynEditMouseActionsLineNum.Create(self);
end;

procedure TSynGutterLineNumber.Paint(Canvas : TCanvas; AClip : TRect; FirstLine, LastLine : integer);
var
  i, c, iLine: integer;
  rcLine: TRect;
  s: string;
  dc: HDC;
  LineInfo: TSynEditGutterLineInfo;
  LineHeight, EveryNLine: Integer;
  t: TLinePos;

begin
  if not Visible then exit;

  LineHeight := SynEdit.LineHeight;
  c := SynEdit.Lines.Count;
  t := ToIdx(GutterArea.TextArea.TopLine);
  // Changed to use fTextDrawer.BeginDrawing and fTextDrawer.EndDrawing only
  // when absolutely necessary.  Note: Never change brush / pen / font of the
  // canvas inside of this block (only through methods of fTextDrawer)!
  dc := Canvas.Handle;
  FTextDrawer.BeginDrawing(dc);
  try
    FTextDrawer.SetBackColor(MarkupInfoInternal.Background);
    fTextDrawer.SetForeColor(MarkupInfoInternal.Foreground);
    fTextDrawer.SetFrameColor(MarkupInfoInternal.FrameColor);
    fTextDrawer.Style := MarkupInfoInternal.Style;
    // prepare the rect initially
    rcLine := AClip;
    rcLine.Bottom := AClip.Top;
    EveryNLine := Max(ShowOnlyLineNumbersMultiplesOf, 1);
    for i := t + FirstLine to t + LastLine do
    begin
      iLine := ToPos(ViewedTextBuffer.DisplayView.ViewToTextIndexEx(i, LineInfo.LineRange));
      if (iLine < 1) or (iLine > c) then break;
      // next line rect
      rcLine.Top := rcLine.Bottom;
      // Must show a dot instead of line number if
      // line number is not the first, the last, the current line
      // or a multiple of ShowOnlyLineNumbersMultiplesOf
      LineInfo.ShowDot := ((iLine mod EveryNLine) <> 0)
          and (iLine <> SynEdit.CaretY) and (iLine <> 1)
          and (iLine <> SynEdit.Lines.Count);
      // Get the formatted line number or dot
      if Assigned(FOnFormatLineNumber) then begin
        LineInfo.ViewLine := i;
        FOnFormatLineNumber(Self, iLine, s, LineInfo);
      end
      else begin
        s := FormatLineNumber(iLine, LineInfo.ShowDot);
      end;
      Inc(rcLine.Bottom, LineHeight);
      if i <> LineInfo.LineRange.Top then
        s := '';
      // erase the background and draw the line number string in one go
      if i - t = CaretRow then begin
        FTextDrawer.SetBackColor(MarkupInfoCurLineMerged.Background);
        fTextDrawer.SetForeColor(MarkupInfoCurLineMerged.Foreground);
        fTextDrawer.SetFrameColor(MarkupInfoCurLineMerged.FrameColor);
        fTextDrawer.Style := MarkupInfoCurLineMerged.Style;
      end
      else
      if i - t = CaretRow+1 then begin
        FTextDrawer.SetBackColor(MarkupInfoInternal.Background);
        fTextDrawer.SetForeColor(MarkupInfoInternal.Foreground);
        fTextDrawer.SetFrameColor(MarkupInfoInternal.FrameColor);
        fTextDrawer.Style := MarkupInfoInternal.Style;
      end;
      fTextDrawer.ExtTextOut(rcLine.Left, rcLine.Top, ETO_OPAQUE or ETO_CLIPPED, rcLine,
        PChar(Pointer(S)),Length(S));
    end;

    // now erase the remaining area if any
    if AClip.Bottom > rcLine.Bottom then
    begin
      FTextDrawer.SetBackColor(MarkupInfoInternal.Background);
      fTextDrawer.SetForeColor(MarkupInfoInternal.Foreground);
      fTextDrawer.SetFrameColor(MarkupInfoInternal.FrameColor);
      fTextDrawer.Style := MarkupInfoInternal.Style;
      rcLine.Top := rcLine.Bottom;
      rcLine.Bottom := AClip.Bottom;
      with rcLine do
        fTextDrawer.ExtTextOut(Left, Top, ETO_OPAQUE, rcLine, nil, 0);
    end;
  finally
    fTextDrawer.EndDrawing;
  end;
end;

end.

