{

    "SHEdit" - Text editor with syntax highlighting
    Copyright (C) 1999-2000 by Sebastian Guenther (sg@freepascal.org)

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program 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.
}

// Generic text document class

unit doc_text;

{$MODE objfpc}
{$H+}

interface

uses Classes;

type
  PLine = ^TLine;
  TLine = record
    info: Pointer;
    flags: LongWord;
    s: AnsiString;
  end;

  PLineArray = ^TLineArray;
  TLineArray = array[0..MaxInt div SizeOf(TLine) - 1] of TLine;

const

  {TLine.flags Syntax Highlighting Flags}
  LF_SH_Valid      = $01;
  LF_SH_Multiline1 = $02;
  LF_SH_Multiline2 = $04;
  LF_SH_Multiline3 = $08;
  LF_SH_Multiline4 = $10;
  LF_SH_Multiline5 = $20;
  LF_SH_Multiline6 = $40;
  LF_SH_Multiline7 = $80;

  {Escape character for syntax highlighting (marks start of sh sequence,
   next character is color/sh element number, beginning at #1}
  LF_Escape = #10;

type

  TTextDoc = class;

  TDocLineEvent = procedure(Sender: TTextDoc; Line: Integer) of object;

  TViewInfo = class(TCollectionItem)
  public
    OnLineInsert, OnLineRemove, OnLineChange: TDocLineEvent;
    OnClearDocument, OnModifiedChange: TNotifyEvent;
  end;

  TTextDoc = class
  protected
    RefCount: LongInt;
    FLineEnding: String;
    FModified: Boolean;
    FLineWidth,
    FLineCount: LongInt;
    FLines: PLineArray;
    FViewInfos: TCollection;
    procedure SetModified(AModified: Boolean);
    function  GetLineText(LineNumber: Integer): String;
    procedure SetLineText(LineNumber: Integer; const NewText: String);
    function  GetLineLen(LineNumber: Integer): Integer;
    function  GetLineFlags(LineNumber: Integer): Byte;
    procedure SetLineFlags(LineNumber: Integer; NewFlags: Byte);
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddRef;
    procedure Release;
    procedure Clear;
    procedure LoadFromStream(AStream: TStream);
    procedure LoadFromFile(const filename: String);
    procedure SaveToStream(AStream: TStream);
    procedure SaveToFile(const filename: String);

    procedure InsertLine(BeforeLine: Integer; const s: String);
    procedure AddLine(const s: String);
    procedure RemoveLine(LineNumber: Integer);

    property LineEnding: String read FLineEnding write FLineEnding;
    property Modified: Boolean read FModified write SetModified;
    property LineWidth: Integer read FLineWidth;
    property LineCount: Integer read FLineCount;
    property LineText[LineNumber: Integer]: String read GetLineText write SetLineText;
    property LineLen[LineNumber: Integer]: Integer read GetLineLen;
    property LineFlags[LineNumber: Integer]: Byte read GetLineFlags write SetLineFlags;

    property ViewInfos: TCollection read FViewInfos;
  end;


implementation

uses Strings;


constructor TTextDoc.Create;
begin
  FModified := False;
{$IFDEF Unix}
  LineEnding := #10;
{$ELSE}
  LineEnding := #13#10;
{$ENDIF}
  FLines := nil;
  FLineCount := 0;
  FLineWidth := 0;
  FViewInfos := TCollection.Create(TViewInfo);
  RefCount := 1;
end;

destructor TTextDoc.Destroy;
var
  i: Integer;
begin
  if Assigned(FLines) then
  begin
    for i := 0 to FLineCount - 1 do
      SetLength(FLines^[i].s, 0);
    FreeMem(FLines);
  end;

  FViewInfos.Free;
  inherited Destroy;
end;

procedure TTextDoc.AddRef;
begin
  Inc(RefCount);
end;

procedure TTextDoc.Release;
begin
  ASSERT(RefCount > 0);
  Dec(RefCount);
  if RefCount = 0 then
    Self.Free;
end;

procedure TTextDoc.Clear;
var
  i: Integer;
begin
  if Assigned(FLines) then
  begin
    for i := 0 to FLineCount - 1 do
      SetLength(FLines^[i].s, 0);
    FreeMem(FLines);
    FLineCount:=0;
  end;

  FLineWidth:=0;

  for i := 0 to FViewInfos.Count - 1 do
    if Assigned(TViewInfo(FViewInfos.Items[i]).OnClearDocument) then
      TViewInfo(FViewInfos.Items[i]).OnClearDocument(Self);
end;

procedure TTextDoc.InsertLine(BeforeLine: Integer; const s: String);
var
  l: PLine;
  i: Integer;
begin
  if (BeforeLine < 0) or (BeforeLine > FLineCount) then
    exit;  // !!!: throw an exception

  ReAllocMem(FLines, (FLineCount + 1) * SizeOf(TLine));
  Move(FLines^[BeforeLine], FLines^[BeforeLine + 1], (FLineCount - BeforeLine) * SizeOf(TLine));
  l := @FLines^[BeforeLine];
  FillChar(l^, SizeOf(TLine), 0);
  l^.s := s;

  Inc(FLineCount);
  if Length(s) > FLineWidth then
    FLineWidth := Length(s);

  for i := 0 to FViewInfos.Count - 1 do
    if Assigned(TViewInfo(FViewInfos.Items[i]).OnLineInsert) then
      TViewInfo(FViewInfos.Items[i]).OnLineInsert(Self, BeforeLine);
end;

procedure TTextDoc.AddLine(const s: String);
begin
  InsertLine(FLineCount, s);
end;

procedure TTextDoc.RemoveLine(LineNumber: Integer);
var
  i: Integer;
begin
  SetLength(FLines^[LineNumber].s, 0);  // Free the string for this line
  ReAllocMem(FLines, (FLineCount - 1) * SizeOf(TLine));
  if LineNumber < FLineCount - 1 then
    Move(FLines^[LineNumber + 1], FLines^[LineNumber],(FLineCount - LineNumber - 1) * SizeOf(TLine));
  Dec(FLineCount);

  for i := 0 to FViewInfos.Count - 1 do
    if Assigned(TViewInfo(FViewInfos.Items[i]).OnLineRemove) then
      TViewInfo(FViewInfos.Items[i]).OnLineRemove(Self, LineNumber);
  Modified := True;
end;

procedure TTextDoc.LoadFromStream(AStream: TStream);

  procedure ProcessLine(const s: String);
  var
    s2: String;
    i: Integer;
  begin
    // Expand tabs to spaces
    s2 := '';
    for i := 1 to Length(s) do
      if s[i] = #9 then
      begin
        repeat
          s2 := s2 + ' '
        until (Length(s2) mod 8) = 0;
      end else
        s2 := s2 + s[i];
    AddLine(s2);
  end;

var
  NewData: array[0..1023] of Byte;
  buffer, p: PChar;
  BytesInBuffer, BytesRead, OldBufSize, LastEndOfLine, i, LineLength: Integer;
  line: String;
begin
  Clear;
  SetLength(line, 0);
  BytesInBuffer := 0;
  buffer := nil;

  while True do
  begin
    BytesRead := AStream.Read(NewData, SizeOf(NewData));
    if BytesRead <= 0 then
      break;
    OldBufSize := BytesInBuffer;

    // Append the new received data to the read buffer
    Inc(BytesInBuffer, BytesRead);
    ReallocMem(buffer, BytesInBuffer);
    Move(NewData, buffer[OldBufSize], BytesRead);

    LastEndOfLine := 0;
    if OldBufSize > 0 then
      i := OldBufSize - 1
    else
      i := 0;

    while i <= BytesInBuffer - 2 do
    begin
      if (buffer[i] = #13) or (buffer[i] = #10) then
      begin
        LineLength := i - LastEndOfLine;
        SetLength(line, LineLength);
        if LineLength > 0 then
          Move(buffer[LastEndOfLine], line[1], LineLength);

        ProcessLine(line);

        if ((buffer[i] = #13) and (buffer[i + 1] = #10)) or
           ((buffer[i] = #10) and (buffer[i + 1] = #13)) then
          Inc(i);
        LastEndOfLine := i + 1;
      end;
      Inc(i);
    end;

    if LastEndOfLine > 0 then
    begin
      // Remove all processed lines from the buffer
      Dec(BytesInBuffer, LastEndOfLine);
      GetMem(p, BytesInBuffer);
      Move(buffer[LastEndOfLine], p^, BytesInBuffer);
      FreeMem(buffer);
      buffer := p;
    end;
  end;

  if BytesInBuffer > 0 then
    if buffer[BytesInBuffer - 1] in [#13, #10] then
    begin
      SetLength(line, BytesInBuffer - 1);
      if BytesInBuffer > 1 then
        Move(buffer^, line[1], BytesInBuffer - 1);
      ProcessLine(line);
      ProcessLine('');
    end else
    begin
      SetLength(line, BytesInBuffer);
      if BytesInBuffer > 1 then
        Move(buffer^, line[1], BytesInBuffer);
      ProcessLine(line);
    end;

  if Assigned(buffer) then
    FreeMem(buffer);
end;


procedure TTextDoc.LoadFromFile(const filename: String);
var
  stream: TFileStream;
begin
  stream := TFileStream.Create(filename, fmOpenRead);
  LoadFromStream(stream);
  stream.Free;
end;

procedure TTextDoc.SaveToStream(AStream: TStream);
var
  i: Integer;
begin
  for i := 0 to FLineCount - 2 do
  begin
    AStream.Write(FLines^[i].s, Length(FLines^[i].s));
    AStream.Write(FLineEnding, Length(FLineEnding));
  end;
  if FLineCount > 0 then
    AStream.Write(FLines^[FLineCount - 1].s, Length(FLines^[FLineCount - 1].s));
end;

procedure TTextDoc.SaveToFile(const filename: String);
var
  stream: TFileStream;
begin
  stream := TFileStream.Create(filename, fmCreate);
  SaveToStream(stream);
  stream.Free;
end;

procedure TTextDoc.SetModified(AModified: Boolean);
var
  i: Integer;
begin
  if AModified = FModified then
    exit;
  FModified := AModified;

  for i := 0 to FViewInfos.Count - 1 do
    if Assigned(TViewInfo(FViewInfos.Items[i]).OnModifiedChange) then
      TViewInfo(FViewInfos.Items[i]).OnModifiedChange(Self);
end;

function TTextDoc.GetLineText(LineNumber: Integer): String;
begin
  if (LineNumber < 0) or (LineNumber >= FLineCount) then
    Result := ''
  else
    Result := FLines^[LineNumber].s;
end;

procedure TTextDoc.SetLineText(LineNumber: Integer; const NewText: String);
var
  i: Integer;
begin
  if FLines^[LineNumber].s <> NewText then
  begin
    FLines^[LineNumber].s := NewText;
    if Length(NewText) > FLineWidth then
      FLineWidth := Length(NewText);
    Modified := True;
    for i := 0 to FViewInfos.Count - 1 do
      if Assigned(TViewInfo(FViewInfos.Items[i]).OnLineChange) then
        TViewInfo(FViewInfos.Items[i]).OnLineChange(Self, LineNumber);
  end;
end;

function TTextDoc.GetLineLen(LineNumber: Integer): Integer;
begin
  if (LineNumber < 0) or (LineNumber >= FLineCount) then
    Result := 0
  else
    Result := Length(FLines^[LineNumber].s);
end;

function TTextDoc.GetLineFlags(LineNumber: Integer): Byte;
begin
  if (LineNumber < 0) or (LineNumber >= FLineCount) then
    Result := 0
  else
    Result := FLines^[LineNumber].flags;
end;

procedure TTextDoc.SetLineFlags(LineNumber: Integer; NewFlags: Byte);
begin
  FLines^[LineNumber].flags := NewFlags;
end;


end.
