{-----------------------------------------------------------------------------
The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: JvUnicodeEditor.PAS, released on 2004-01-25.

The Initial Developers of the Original Code are: Andrei Prygounkov <a dott prygounkov att gmx dott de>
Copyright (c) 1999, 2002 Andrei Prygounkov
All Rights Reserved.

Contributor(s):
Burov Dmitry, translation of russian text.
Andreas Hausladen
Peter Thörnqvist
Remko Bonte

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.delphi-jedi.org

component   : TJvEditor
description : 'Delphi IDE'-like Editor (unicode)

Known Issues:
  Some russian comments were translated to english; these comments are marked
  with [translated]
-----------------------------------------------------------------------------}
// $Id$

unit JvUnicodeEditor;

{$I jvcl.inc}

interface

uses
  {$IFDEF UNITVERSIONING}
  JclUnitVersioning,
  {$ENDIF UNITVERSIONING}
  Windows, Messages, Classes, Controls,
  JclWideStrings,
  JvEditorCommon;

type
  TJvCustomWideEditor = class;

  TJvEditorWideStrings = class(TWStringList)
  private
    FJvEditor: TJvCustomWideEditor;
    procedure StringsChanged(Sender: TObject);
    procedure SetInternal(Index: Integer; const Value: WideString);
    procedure ReLine;
    procedure SetLockText(const Text: WideString);
  protected
    procedure Put(Index: Integer; const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE}); override;
    procedure InternalPut(Index: Integer; const Value: WideString);

    property Internal[Index: Integer]: WideString write SetInternal;
    property JvEditor: TJvCustomWideEditor read FJvEditor;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure AddStrings(Strings: TWStrings); override;
    procedure SetTextStr(const Value: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE}); override;
    function Add(const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE}): Integer; override;
    procedure Insert(Index: Integer; const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE}); override;
    procedure Delete(Index: Integer); override;
    procedure DeleteText(BegX, BegY, EndX, EndY: Integer);
    procedure InsertText(X, Y: Integer; const Text: WideString);
    procedure DeleteColumnText(BegX, BegY, EndX, EndY: Integer);
    procedure InsertColumnText(X, Y: Integer; const Text: WideString);
  end;

  TJvWideCompletion = class;
  TOnCompletionApplyW = procedure(Sender: TObject; const OldString: WideString;
    var NewString: WideString) of object;
  TJvGetLineAttrEventW = procedure(Sender: TObject; var Line: WideString; Index: Integer;
    var Attrs: TLineAttrs) of object;

  TJvCustomWideEditor = class(TJvCustomEditorBase)
  private
    {$IFNDEF SUPPORTS_UNICODE}
    FCodePage: Cardinal;
    {$ENDIF ~SUPPORTS_UNICODE}
    { internal objects }
    FLines: TJvEditorWideStrings;

    { events }
    FOnGetLineAttr: TJvGetLineAttrEventW;
    FOnCompletionApply: TOnCompletionApplyW;

    {$IFNDEF SUPPORTS_UNICODE}
    procedure WMInputLangChange(var Msg: TMessage); message WM_INPUTLANGCHANGE;
    {$ENDIF ~SUPPORTS_UNICODE}

    { get/set for properties }
    function GetLines: TWStrings;
    procedure SetLines(ALines: TWStrings);
    function GetCompletion: TJvWideCompletion;
    procedure SetCompletion(const Value: TJvWideCompletion);
  protected
    function GetLineCount: Integer; override;
    function GetLineLength(Index: Integer): Integer; override;
    function FindNotBlankCharPosInLine(Line: Integer): Integer; override;

    function GetTextLine(Y: Integer; out Text: string): Boolean; override;
    function InternGetWordOnCaret: string; override;

    procedure ReLine; override;
    function GetTabStop(X, Y: Integer; Next: Boolean): Integer; override;
    function GetBackStop(X, Y: Integer): Integer; override;
    procedure TextAllChangedInternal(Unselect: Boolean); override;
  protected
    procedure PaintLineText(Line: Integer; ColBeg, ColEnd: Integer;
      var ColPainted: Integer); override;
    procedure InsertChar(const Value: Word); override;
  protected
    procedure SetLockText(const Text: WideString);
    function ExpandTabs(const S: WideString): WideString;
    function GetAutoIndentStop(Y: Integer): Integer; override;

    { triggers for descendants }
    procedure GetLineAttr(var Str: WideString; Line, ColBeg, ColEnd: Integer); virtual;
    function DoCommand(ACommand: TEditCommand; var X, Y: Integer;
      var CaretUndo: Boolean): Boolean; override;
    { TextModified is called when the editor content has changed. }
    procedure TextModified(ACaretX, ACaretY: Integer; Action: TModifiedAction;
      const Text: WideString); dynamic;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure ClipboardCopy; override;
    procedure ClipboardPaste; override;
    procedure DeleteSelected; override;
    procedure Clear;

    function GetSelText: WideString;
    procedure SetSelText(const AValue: WideString);
    function GetWordOnCaret: WideString;
    procedure SelectWordOnCaret; override;
    function GetText: string; override;

    procedure InsertText(const Text: WideString);
    procedure InsertColumnText(X, Y: Integer; const Text: WideString);
    procedure ReplaceWord(const NewString: WideString);
    procedure ReplaceWord2(const NewString: WideString);
    procedure IndentColumns(X: Integer; BegY, EndY: Integer); override;
    procedure UnIndentColumns(X: Integer; BegY, EndY: Integer); override;

    property SelText: WideString read GetSelText write SetSelText;
  public
    { published in descendants }
    property Lines: TWStrings read GetLines write SetLines;
    property Completion: TJvWideCompletion read GetCompletion write SetCompletion;
    property OnGetLineAttr: TJvGetLineAttrEventW read FOnGetLineAttr write FOnGetLineAttr;
    property OnCompletionApply: TOnCompletionApplyW read FOnCompletionApply write FOnCompletionApply;
  end;

  {$IFDEF RTL230_UP}
  [ComponentPlatformsAttribute(pidWin32 or pidWin64)]
  {$ENDIF RTL230_UP}
  TJvWideEditor = class(TJvCustomWideEditor)
  published
    property BeepOnError;
    property BorderStyle;
    property Lines;
    property ScrollBars;
    property GutterWidth;
    property GutterColor;
    property RightMarginVisible;
    property RightMargin;
    property RightMarginColor;
    property InsertMode;
    property ReadOnly;
    property DoubleClickLine;
    property HideCaret;
    property Completion;
    property TabStops;
    property SmartTab;
    property BackSpaceUnindents;
    property AutoIndent;
    property KeepTrailingBlanks;
    property CursorBeyondEOF;
    property CursorBeyondEOL;
    property BracketHighlighting;
    property SelForeColor;
    property SelBackColor;
    property SelBlockFormat;

    property OnGetLineAttr;
    property OnChangeStatus;
    property OnScroll;
    property OnResize;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnChange;
    property OnCaretChanged;
    property OnSelectionChange;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnDblClick;
    property OnPaintGutter;
    property OnCompletionIdentifier;
    property OnCompletionTemplate;
    property OnCompletionDrawItem;
    property OnCompletionMeasureItem;
    property OnCompletionApply;

    { TCustomControl }
    property Align;
    property Enabled;
    property Color;
    property Font;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property TabStop;
    property Visible;
    property Anchors;
    property AutoSize;
    property BiDiMode;
    property Constraints;
    property UseDockManager;
    property DockSite;
    property DragKind;
    property ParentBiDiMode;
    property OnCanResize;
    property OnConstrainedResize;
    property OnDockDrop;
    property OnDockOver;
    property OnEndDock;
    property OnGetSiteInfo;
    property OnStartDock;
    property OnUnDock;
    property OnEnter;
    property OnExit;
  end;

  TJvWideCompletion = class(TJvCompletionBase)
  private
    FIdentifiers: TWStrings;
    FTemplates: TWStrings;
    FCaretChar: WideChar;
    FCRLF: WideString;
    FSeparator: WideString;
    function GetStrings(Index: Integer): TWStrings;
    procedure SetStrings(Index: Integer; AValue: TWStrings);
    procedure ReplaceWord(const NewString: WideString);
  protected
    procedure FindSelItem(var Eq: Boolean); override;
    procedure MakeItems; override;
    procedure ReplaceWordItemIndex(SubStrStart: Integer); override;
    function GetTemplateCount: Integer; override;
    function GetIdentifierCount: Integer; override;
    function GetSeparator: string; override;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor);
    destructor Destroy; override;
  published
    property Identifiers: TWStrings index 0 read GetStrings write SetStrings;
    property Templates: TWStrings index 1 read GetStrings write SetStrings;
    property CaretChar: WideChar read FCaretChar write FCaretChar default '|';
    property CRLF: WideString read FCRLF write FCRLF;
    property Separator: WideString read FSeparator write FSeparator;
  end;

{$IFDEF UNITVERSIONING}
const
  UnitVersioning: TUnitVersionInfo = (
    RCSfile: '$URL$';
    Revision: '$Revision$';
    Date: '$Date$';
    LogPath: 'JVCL\run'
  );
{$ENDIF UNITVERSIONING}

implementation

uses
  SysUtils, Math, Graphics, Clipbrd,
  JvUnicodeCanvas, JvJCLUtils, JvConsts, JvResources;

type
  TJvInsertUndo = class(TJvCaretUndo)
  private
    FText: WideString;
    function GetEditor: TJvCustomWideEditor;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      const AText: WideString);
    procedure Undo; override;
  end;

  TJvOverwriteUndo = class(TJvCaretUndo)
  private
    FOldText: WideString;
    FNewText: WideString;
    function GetEditor: TJvCustomWideEditor;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      const AOldText, ANewText: WideString);
    procedure Undo; override;
  end;

  TJvReLineUndo = class(TJvInsertUndo, IJvUndoCompound);

  TJvInsertTabUndo = class(TJvInsertUndo);

  TJvInsertColumnUndo = class(TJvInsertUndo)
  public
    procedure Undo; override;
  end;

  TJvDeleteUndo = class(TJvInsertUndo)
  public
    procedure Undo; override;
  end;

  TJvDeleteLineUndo = class(TJvInsertUndo)
  private
    FLastLineDelete: Boolean;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      const AText: WideString; ALastLineDelete: Boolean);
    procedure Undo; override;
    //procedure Redo; override;
  end;

  TJvDeleteTrailUndo = class(TJvDeleteUndo, IJvUndoCompound);

  TJvBackspaceUndo = class(TJvDeleteUndo, IJvBackspaceUndo)
  public
    procedure Undo; override;
  end;

  TJvBackspaceUnindentUndo = class(TJvDeleteUndo, IJvBackspaceUnindentUndo)
  public
    procedure Undo; override;
  end;

  TJvReplaceUndo = class(TJvCaretUndo)
  private
    FBegX: Integer;
    FBegY: Integer;
    FText: WideString;
    FNewText: WideString;
    function GetEditor: TJvCustomWideEditor;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      ABegX, ABegY: Integer; const AText, ANewText: WideString);
    procedure Undo; override;
  end;

  TJvDeleteSelectedUndo = class(TJvDeleteUndo)
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      const AText: WideString);
    procedure Undo; override;
  end;

(* // (ahuser) make Delphi 5 compiler happy
  TJvIndentColumnUndo = class(TJvInsertColumnUndo)
  private
    FNewCaretX: Integer;
    FNewCaretY: Integer;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
      ABegX, ABegY: Integer; const AText: WideString);
    procedure Undo; override;
  end;
*)

  TJvUnindentColumnUndo = class(TJvInsertUndo)
  private
    FBegX: Integer;
    FBegY: Integer;
  public
    constructor Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY,
      ABegX, ABegY: Integer; const AText: WideString);
    procedure Undo; override;
  end;

  TJvUndoBufferAccessProtected = class(TJvUndoBuffer);

//=== { TJvEditorWideStrings } ===============================================

constructor TJvEditorWideStrings.Create;
begin
  inherited Create;
  OnChange := StringsChanged;
end;

destructor TJvEditorWideStrings.Destroy;
begin
  OnChange := nil;
  OnChanging := nil;
  inherited Destroy;
end;

procedure TJvEditorWideStrings.Assign(Source: TPersistent);
begin
  JvEditor.BeginUpdate;
  try
    inherited Assign(Source);
    JvEditor.NotUndoable;
    JvEditor.TextAllChanged;
  finally
    JvEditor.EndUpdate;
  end;
end;

procedure TJvEditorWideStrings.AddStrings(Strings: TWStrings);
begin
  JvEditor.BeginUpdate;
  try
    inherited AddStrings(Strings);
    JvEditor.NotUndoable;
  finally
    JvEditor.EndUpdate;
  end;
end;

procedure TJvEditorWideStrings.SetTextStr(const Value: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE});
begin
  inherited SetTextStr(JvEditor.ExpandTabs(Value));
  if JvEditor.UpdateLock = 0 then
    JvEditor.NotUndoable;
  JvEditor.TextAllChanged;
end;

procedure TJvEditorWideStrings.StringsChanged(Sender: TObject);
begin
  if JvEditor.UpdateLock = 0 then
    JvEditor.TextAllChanged;
end;

procedure TJvEditorWideStrings.SetLockText(const Text: WideString);
begin
  JvEditor.LockUpdate;
  try
    inherited SetTextStr(Text)
  finally
    JvEditor.UnlockUpdate;
  end;
end;

procedure TJvEditorWideStrings.SetInternal(Index: Integer; const Value: WideString);
begin
  JvEditor.LockUpdate;
  try
    InternalPut(Index, Value);
  finally
    JvEditor.UnlockUpdate;
  end;
end;

function TJvEditorWideStrings.Add(const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE}): Integer;
begin
  Result := inherited Add(JvEditor.ExpandTabs(S));
end;

procedure TJvEditorWideStrings.Insert(Index: Integer; const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE});
begin
  inherited Insert(Index, JvEditor.ExpandTabs(S));
  JvEditor.LineInserted(Index);
end;

procedure TJvEditorWideStrings.Delete(Index: Integer);
begin
  inherited Delete(Index);
  JvEditor.LineDeleted(Index);
end;

procedure TJvEditorWideStrings.Put(Index: Integer; const S: {$IFDEF SUPPORTS_UNICODE}string{$ELSE}WideString{$ENDIF SUPPORTS_UNICODE});
var
  L: Integer;
begin
  if JvEditor.KeepTrailingBlanks then
    inherited Put(Index, S)
  else
  begin
    L := Length(S) - TrimRightLengthW(S);
    if L = 0 then
      inherited Put(Index, S)
    else
    begin
      {--- UNDO ---}
      TJvDeleteTrailUndo.Create(JvEditor, Length(S), Index, SpacesW(L));
      {--- /UNDO ---}
      inherited Put(Index, TrimRightW(S));
    end;
  end;
end;

procedure TJvEditorWideStrings.ReLine;
var
  L: Integer;
  S: WideString;
begin
  JvEditor.LockUpdate;
  try
    if Count = 0 then
      L := JvEditor.CaretX
    else
      L := Length(Strings[Count - 1]);
    while JvEditor.CaretY > Count - 1 do
    begin
      {--- UNDO ---}
      TJvReLineUndo.Create(JvEditor, L, JvEditor.CaretY, sLineBreakStr);
      {--- /UNDO ---}
      L := 0;
      Add('');
    end;
    S := Strings[JvEditor.CaretY];
    if JvEditor.CaretX > Length(S) then
    begin
      L := JvEditor.CaretX - Length(S);
      {--- UNDO ---}
{     TJvReLineUndo.Create(JvEditor, Length(S),
        JvEditor.CaretY, SpacesW(L)); } {disabled: moves the caret to wrong undo position }
      {--- /UNDO ---}
      inherited Put(JvEditor.CaretY, S + SpacesW(L));
    end;
  finally
    JvEditor.UnlockUpdate;
  end;
end;

procedure TJvEditorWideStrings.InternalPut(Index: Integer; const Value: WideString);
begin
  if JvEditor.KeepTrailingBlanks then
    inherited Put(Index, JvEditor.ExpandTabs(Value))
  else
    inherited Put(Index, TrimRightW(JvEditor.ExpandTabs(Value)));
end;

procedure TJvEditorWideStrings.DeleteText(BegX, BegY, EndX, EndY: Integer);
{ delete text from [BegX..EndY] [BegY..EndY] all inclusive.
  BegX,EndX: [0..Max_X] }
var
  BegLine, EndLine: WideString;
  I, L: Integer;
begin
  if BegY < 0 then
  begin
    BegY := 0;
    BegX := 0;
  end;
  if BegY >= Count then
    Exit; // nothing to delete
  if EndY >= Count then
  begin
    EndY := Count - 1;
    EndX := MaxInt - 1;
  end;
  if BegX < 0 then
    BegX := 0;

  JvEditor.LockUpdate;
  BeginUpdate;
  try
    BegLine := Strings[BegY];
   // expand BegLine if necessary
    L := (BegX + 1) - Length(BegLine) - 1;
    if L > 0 then
      BegLine := BegLine + SpacesW(L);

    EndLine := Strings[EndY];

    // delete lines between and end line
    for I := EndY downto BegY + 1 do
      Delete(I);

    System.Delete(BegLine, BegX + 1, MaxInt);
    System.Delete(EndLine, 1, EndX + 1);

    Internal[BegY] := BegLine + EndLine;
  finally
    EndUpdate;
    JvEditor.UnlockUpdate;
  end;
end;

procedure TJvEditorWideStrings.InsertText(X, Y: Integer; const Text: WideString);
{ insert text on X:[0..Max_X], Y }
var
  BegLine, EndLine: WideString;
  YStart: Integer;
  F, P: PWideChar;
  S, FirstLine: WideString;
  Len: Integer;
begin
  Inc(X); // increment for WideString functions
  if Y < 0 then
    Y := 0;
  while Y >= Count do
    Add('');

  BegLine := Strings[Y];
  EndLine := System.Copy(BegLine, X, MaxInt);
  System.Delete(BegLine, X, MaxInt);

  // line is too small -> expand it with spaces
  Len := Length(BegLine);
  if Len < X then
  begin
    SetLength(BegLine, X - 1);
    P := PWideChar(BegLine) + Len;
    Len := X - Len - 1;
    while Len > 0 do
    begin
      P^ := ' ';
      Inc(P);
      Dec(Len);
    end;
  end;

  JvEditor.LockUpdate;
  BeginUpdate;
  try
    P := PWideChar(Text);
    F := P;
    while (P[0] <> #0) and (P[0] <> Lf) and (P[0] <> Cr) do
      Inc(P);

    SetString(S, F, P - F);

    YStart := Y;
    FirstLine := BegLine + S; // set Internal[YStart] later so we keep the trailing spaces for concat EndLine

    while P[0] <> #0 do
    begin
      if P[0] = Cr then
        Inc(P);
      if P[0] = Lf then
        Inc(P);
      F := P;

      while (P[0] <> #0) and (P[0] <> Lf) and (P[0] <> Cr) do
        Inc(P);
      SetString(S, F, P - F);
      Inc(Y);
      Insert(Y, S);
    end;

    if Y = YStart then
      Internal[YStart] := FirstLine + EndLine
    else
    begin
      Internal[YStart] := FirstLine;
      Internal[Y] := Strings[Y] + EndLine;
    end;
  finally
    EndUpdate;
    JvEditor.UnlockUpdate;
  end;
end;

procedure TJvEditorWideStrings.DeleteColumnText(BegX, BegY, EndX, EndY: Integer);
{ delete column text from [BegX..EndY] [BegY..EndY] all inclusive.
  BegX,EndX: [0..Max_X] }
var
  S: WideString;
  I: Integer;
begin
  if BegY < 0 then
  begin
    BegY := 0;
    BegX := 0;
  end;
  if BegY >= Count then
    Exit; // nothing to delete
  if EndY >= Count then
  begin
    EndY := Count - 1;
    EndX := MaxInt - 1;
  end;
  if BegX < 0 then
    BegX := 0;

  JvEditor.LockUpdate;
  BeginUpdate;
  try
    for I := BegY to EndY do
    begin
      S := JvEditor.FLines[I];
      System.Delete(S, BegX + 1, EndX - BegX + 1);
      JvEditor.FLines.Internal[I] := S;
    end;
  finally
    EndUpdate;
    JvEditor.UnlockUpdate;
  end;
end;

procedure TJvEditorWideStrings.InsertColumnText(X, Y: Integer; const Text: WideString);
{ insert column text on X:[0..Max_X], Y }
var
  S, Line: WideString;
  P, F: PWideChar;
  L: Integer;
begin
  Inc(X); // increment for WideString functions
  if Y < 0 then
    Y := 0;

  JvEditor.LockUpdate;
  BeginUpdate;
  try
    P := PWideChar(Text);
    F := P;
    while P[0] <> #0 do
    begin
      while (P[0] <> #0) and (P[0] <> Lf) and (P[0] <> Cr) do
        Inc(P);
      SetString(S, F, P - F);

      while Y >= Count do
        Add('');
      Line := Strings[Y];
      L := (X - 1) - Length(Line);
      if L > 0 then
        Line := Line + SpacesW(L);
      System.Insert(S, Line, X);
      Internal[Y] := Line;

      if P[0] = Cr then
        Inc(P);
      if P[0] = Lf then
        Inc(P);
      F := P;
      Inc(Y);
    end;
  finally
    EndUpdate;
    JvEditor.UnlockUpdate;
  end;
end;

//=== { TJvCustomWideEditor } ================================================

constructor TJvCustomWideEditor.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FLines := TJvEditorWideStrings.Create;
  FLines.FJvEditor := Self;
  FLines.OnChange := DoLinesChange;
  Completion := TJvWideCompletion.Create(Self);
  {$IFNDEF SUPPORTS_UNICODE}
  FCodePage := CP_ACP;
  {$ENDIF ~SUPPORTS_UNICODE}
end;

destructor TJvCustomWideEditor.Destroy;
begin
  FLines.Free;
  Completion.Free;
  inherited Destroy;
end;

procedure TJvCustomWideEditor.PaintLineText(Line: Integer; ColBeg, ColEnd: Integer;
  var ColPainted: Integer);
var
  Ch: WideString;
  iC, jC, SL, MX: Integer;
  R: TRect;
  S: WideString;
  LA: TLineAttr;
  jCStart, Len: Integer;
  MyDi: TDynIntArray;
  ColCount: Integer;
  {$IFDEF COMPILER12_UP}
  CharSize: Integer;
  {$ENDIF COMPILER12_UP}
begin
  with EditorClient do
  begin
    S := FLines[Line];

    Len := Max(Length(S), Max_X) + 1; // LineAttrs is used as 1-based array => one element more needed
    if Len > Length(LineAttrs) then
      SetLength(LineAttrs, Len);

    if ColEnd >= Len then
      ColEnd := Len - 1;

    GetLineAttr(S, Line, ColBeg, ColEnd);

    {left line}
    if Canvas.Brush.Color <> LineAttrs[LeftCol + 1].BC then // change GDI object only if necessary
      Canvas.Brush.Color := LineAttrs[LeftCol + 1].BC;

    Canvas.FillRect(Bounds(EditorClient.Left, (Line - TopRow) *
      CellRect.Height, 1, CellRect.Height));
    {optimized, paint group of chars with identical attributes}
    SL := Length(S);
    MX := ColEnd;

    if Length(FMyDi) < MX then
    begin
      SetLength(MyDi, MX);
      for iC := 0 to High(MyDi) do
        MyDi[iC] := CellRect.Width;
     end
    else
      MyDi := FMyDi;

    while ColPainted < MX do
    begin
      with Canvas do
      begin
        iC := ColPainted + 1;
        LA := LineAttrs[iC];
        jC := iC + 1;
        if iC <= SL then
          Ch := S[iC]
        else
          Ch := ' ';
        jCStart := jC;
        while (jC <= MX + 1) and CompareMem(@LA, @LineAttrs[jC], SizeOf(LineAttrs[1])) do
          Inc(jC);
        Ch := Copy(S, jCStart - 1, jC - jCStart + 1);
        if jC > SL + 1 then
          Ch := Ch + Spaces(jC - SL - 1);

        if Brush.Color <> LA.BC then // change GDI object only if necessary
          Brush.Color := LA.BC;
        Font.Assign(FontCacheFind(LA));

        ColCount := 0;
        {$IFDEF COMPILER12_UP}
        for iC := 0 to High(MyDi) - 1 do
        begin
          {TODO: a cache for the TextWidth() call should be used and cleared if the font name changes. }
          if (iC < Len) and (Ord(Ch[iC + 1]) >= 256) then
          begin
            CharSize := ((EditorClient.Canvas.TextWidth(Ch[iC + 1]) + (CellRect.Width - 1)) div CellRect.Width);
            MyDi[iC] := CellRect.Width * CharSize;
            Inc(ColCount, CharSize - 1);
          end
          else
            MyDi[iC] := CellRect.Width;
        end;
        {$ENDIF COMPILER12_UP}
        Inc(ColCount, Len);

        R := CalcCellRect(ColPainted - LeftCol, Line - TopRow);
        {bottom line}
        FillRect(Bounds(R.Left, R.Bottom - 1, CellRect.Width * ColCount, 1));

        TJvUnicodeCanvas(Canvas).ExtTextOutW(R.Left, R.Top, [etoOpaque, etoClipped], nil, Ch, @FMyDi[0]);
        ErrorHighlighting.PaintError(Canvas, ColPainted + 1, Line, R, ColCount, MyDi);

        if LA.Border <> clNone then
        begin
          Pen.Color := LA.Border;
          R.Right := R.Left + CellRect.Width * ColCount;
          Dec(R.Left);
          Brush.Style := bsClear;
          Rectangle(R);
          Brush.Style := bsSolid;
        end;

        ColPainted := jC - 1;
      end;
    end;
  end;
end;

procedure TJvCustomWideEditor.GetLineAttr(var Str: WideString; Line, ColBeg, ColEnd: Integer);
var
  I: Integer;
begin
  if ColBeg < 0 then
    ColBeg := 0;
  if ColEnd > Max_X then
    ColEnd := Max_X;

  LineAttrs[ColBeg].Style := Font.Style;
  LineAttrs[ColBeg].FC := Font.Color;
  LineAttrs[ColBeg].BC := Color;
  LineAttrs[ColBeg].Border := clNone;

{  for I := ColBeg + 1 to ColEnd do
    Move(LineAttrs[ColBeg], LineAttrs[I], SizeOf(LineAttrs[1]));}
  for I := ColBeg + 1 to ColEnd do
    LineAttrs[I] := LineAttrs[ColBeg];

  GetAttr(Line, ColBeg, ColEnd);
  if Assigned(FOnGetLineAttr) then
    FOnGetLineAttr(Self, Str, Line, LineAttrs);
  ChangeAttr(Line, ColBeg, ColEnd);
end;

function TJvCustomWideEditor.GetTextLine(Y: Integer; out Text: string): Boolean;
begin
  if (Y >= 0) and (Y < Lines.Count) then
  begin
    Text := Lines[Y];
    Result := True;
  end
  else
  begin
    Text := '';
    Result := False;
  end;
end;

function TJvCustomWideEditor.InternGetWordOnCaret: string;
begin
  Result := GetWordOnCaret;
end;

procedure TJvCustomWideEditor.ReLine;
begin
  FLines.ReLine;
end;

{$IFNDEF SUPPORTS_UNICODE}
procedure TJvCustomWideEditor.WMInputLangChange(var Msg: TMessage);
var
  Buffer: array[0..6] of Char;
begin
  inherited;
  GetLocaleInfo(Msg.LParamLo, LOCALE_IDEFAULTANSICODEPAGE, Buffer, Length(Buffer));
  FCodePage := StrToIntDef(Buffer, CP_ACP);
end;
{$ENDIF ~SUPPORTS_UNICODE}

procedure TJvCustomWideEditor.InsertChar(const Value: Word);
var
  S: WideString;
  X, Y, iBeg: Integer;
  WasSelected: Boolean;
  Key: WideChar;
begin
  {$IFDEF SUPPORTS_UNICODE}
  Key := WideChar(Value);
  {$ELSE}
  MultiByteToWideChar(FCodePage, 0, @Value, 1, @Key, 1);
  {$ENDIF SUPPORTS_UNICODE}

  WasSelected := (FSelection.IsSelected) and (not PersistentBlocks);
  if Value >= 32 then
  begin
    if (Value < 256) and not HasChar(Char(Key), JvEditorCompletionChars) then
      Completion.DoKeyPress(Char(Key));

    RemoveSelectedBlock;

    ReLine; // need ReLine after DeleteSelection
    S := FLines[CaretY];
    if InsertMode then
    begin
      {--- UNDO ---}
      TJvInsertUndo.Create(Self, CaretX, CaretY, Key);
      {--- /UNDO ---}
      Insert(Key, S, CaretX + 1);

      AdjustPersistentBlockSelection(CaretX, CaretY, amInsert, [1]);
    end
    else
    begin
      {--- UNDO ---}
      if CaretX + 1 <= Length(S) then
        TJvOverwriteUndo.Create(Self, CaretX, CaretY, S[CaretX + 1], Key)
      else
        TJvOverwriteUndo.Create(Self, CaretX, CaretY, '', Key);
      {--- /UNDO ---}
      if CaretX + 1 <= Length(S) then
        S[CaretX + 1] := WideChar(Key)
      else
        S := S + Key
    end;
    FLines.Internal[CaretY] := S;
    SetCaretInternal(CaretX + 1, CaretY);
    TextModified(CaretX, CaretY, maInsert, Key);
    PaintLine(CaretY, -1, -1);
    Changed;

    if (Value < 256) and HasChar(Char(Key), JvEditorCompletionChars) then
      Completion.DoKeyPress(Char(Key));
  end
  else
  case Key of
    Cr:
      begin
        if InsertMode then
        begin
          if WasSelected then // compound only on selection deletion
            BeginCompound;
          LockUpdate;
          try
            RemoveSelectedBlock; // adjusts CaretX, CaretY
            X := CaretX;
            Y := CaretY;
            { --- UNDO --- }
            TJvInsertUndo.Create(Self, CaretX, CaretY, sLineBreakStr);
            { --- /UNDO --- }
            if FLines.Count = 0 then
              FLines.Add('');
            ReLine;

            S := Copy(FLines[Y], X + 1, MaxInt);
            FLines.Insert(Y + 1, S);
            FLines.Internal[Y] := Copy(FLines[Y], 1, X);
            Inc(Y);
            { auto indent }
            if AutoIndent and
              (((Length(FLines[CaretY]) > 0) and
              (FLines[CaretY][1] = ' ')) or
              ((TrimW(FLines[CaretY]) = '') and (X > 0))) then
            begin
              X := GetAutoIndentStop(Y);
              if X > 0 then
              begin
                { --- UNDO --- }
                TJvInsertUndo.Create(Self, 0, Y, SpacesW(X));
                { --- /UNDO --- }
                FLines.Internal[Y] := SpacesW(X) + FLines[Y];
              end;
            end
            else
              X := 0;

           // persistent blocks: adjust selection
            AdjustPersistentBlockSelection(CaretX, CaretY, amLineBreak, []);

            UpdateEditorSize;
            TextModified(CaretX - 1, CaretY, maInsert, sLineBreakStr);
          finally
            UnlockUpdate;
            if WasSelected then
              EndCompound;
          end;
          Invalidate;
          Changed;
        end
        else // Overwrite-mode
        begin
          if WasSelected then // compound only on selection deletion
            BeginCompound;
          try
            RemoveSelectedBlock;
            X := CaretX;
            Y := CaretY;
            Inc(Y);
            if Y >= FLines.Count then
            begin
              LockUpdate;
              try
                { --- UNDO --- }
                TJvInsertUndo.Create(Self, CaretX, CaretY, sLineBreakStr);
                { --- /UNDO --- }
                FLines.Add('');
              finally
                UnlockUpdate;
              end;
              TextModified(0, Y - 1, maInsert, sLineBreakStr);
              UpdateEditorSize;
              Invalidate;
              Changed;
            end;
            if Y < FLines.Count then
            begin
              S := FLines[Y];
              if Length(S) > 0 then
              begin
                iBeg := FindNotBlankCharPos(S) - 1;
                if iBeg < X then
                  X := iBeg;
              end;
            end;
          finally
            if WasSelected then
              EndCompound;
          end;
        end;
        SetCaretInternal(X, Y);
      end;
  end;
end;

procedure TJvCustomWideEditor.SelectWordOnCaret;
var
  iBeg, iEnd: Integer;
begin
  if (CaretY >= 0) and (CaretY < LineCount) and (TrimW(FLines[CaretY]) <> '') then
  begin
    iEnd := Length(TrimRightW(FLines[CaretY]));
    if FCaretX < iEnd then
      while FLines[FCaretY][FCaretX + 1] <= ' ' do
        Inc(FCaretX)
    else
    begin
      FCaretX := iEnd - 1;
      while FLines[FCaretY][FCaretX + 1] <= ' ' do
        Dec(FCaretX);
    end;
    if GetWordOnPosEx(FLines[FCaretY] + ' ', FCaretX + 1, iBeg, iEnd) <> '' then
    begin
      PaintCaret(False);
      SetSel(iBeg - 1, FCaretY);
      SetSel(iEnd - 1, FCaretY);
      SetCaret(iEnd - 1, FCaretY);
      PaintCaret(True);
    end;
  end;
end;

function TJvCustomWideEditor.DoCommand(ACommand: TEditCommand; var X, Y: Integer;
  var CaretUndo: Boolean): Boolean;

type
  TPr = procedure of object;

  procedure DoAndCorrectXY(Pr: TPr);
  begin
    Pr;
    X := CaretX;
    Y := CaretY;
    CaretUndo := False;
  end;

  procedure SetSel1(X, Y: Integer);
  begin
    SetSel(X, Y);
    CaretUndo := False;
  end;

  procedure SetSelText1(const S: WideString);
  begin
    SelText := S;
    CaretUndo := False;
  end;

var
  F: Integer;
  S, S2: WideString;
  B: Boolean;
  iBeg, iEnd: Integer;
begin
  Result := True;
  X := CaretX;
  Y := CaretY;
  case ACommand of
    { caret movements }
    ecPrevWord, ecSelPrevWord, ecBackspaceWord:
      begin
        if (ACommand = ecSelPrevWord) and IsNewSelection then
          SetSel1(CaretX, CaretY);
        if Y >= FLines.Count then
          Exit;

        S := FLines[Y];
        B := False;
        if CaretX > Length(S) then
        begin
          X := Length(S);
          SetSel1(X, Y);
        end
        else
        begin
          for F := X - 1 downto 0 do
          begin
            if B then
            begin
              if CharInSetW(S[F + 1], Separators) then
              begin
                X := F + 1;
                Break;
              end;
            end
            else
            if not CharInSetW(S[F + 1], Separators) then
              B := True;
          end;

          if X = CaretX then
            X := 0;
          if ACommand <> ecBackspaceWord then
          begin
            { Jump to previous line and last word ending }
            if (X = 0) and (Y > 0) then
            begin
              if (Y > FLines.Count) or (CaretX = 0) or (FLines[Y] = '') or
                 CharInSetW(FLines[Y][1], Separators) then
              begin
                Y := Y - 1;
                X := Length(FLines[Y]);
              end;
            end;
          end;

          if ACommand = ecSelPrevWord then
            SetSel1(X, Y)
          else
            PersistentBlocksSetUnSelected;

          if (ACommand = ecBackspaceWord) and (Y >= 0) and (X <> CaretX) then
          begin
            if not ReadOnly then
            begin
              BeginCompound;
              try
                SelectRange(X, CaretY, CaretX, CaretY);
                DeleteSelected;
              finally
                EndCompound;
              end;
              ReLine;
            end;
          end;
        end;
      end;
    ecNextWord, ecSelNextWord:
      begin
        if (ACommand = ecSelNextWord) and IsNewSelection then
          SetSel1(CaretX, CaretY);
        if Y >= FLines.Count then
        begin
          Y := FLines.Count - 1;
          if Y < 0 then
            Exit;
          X := Length(FLines[Y]);
        end;
        S := FLines[Y];
        B := False;
        if CaretX >= Length(S) then
        begin
          if Y < FLines.Count - 1 then
          begin
            Y := CaretY + 1;
            X := 0;
            if Y < FLines.Count then
              while (X < Length(FLines[Y])) and (CharInSetW(FLines[Y][X + 1], Separators)) do
                Inc(X);
            if ACommand = ecSelNextWord then // this code is copied from [ecPrevWord, ecSelPrevWord]
              SetSel1(X, Y)
            else
              PersistentBlocksSetUnSelected;
          end;
        end
        else
        begin
          for F := X to Length(S) - 1 do
            if B then
            begin
              if not CharInSetW(S[F + 1], Separators) then
              begin
                X := F;
                Break;
              end
            end
            else
            if CharInSetW(S[F + 1], Separators) then
              B := True;
          if X = CaretX then
            X := Length(S);
          if ACommand = ecSelNextWord then
            SetSel1(X, Y)
          else
            PersistentBlocksSetUnSelected;
        end;
      end;
    ecSelWord:
      if IsNewSelection and (GetWordOnPosEx(FLines[Y] + ' ', X + 1, iBeg,
        iEnd) <> '') then
      begin
        SetSel1(iBeg - 1, Y);
        SetSel1(iEnd - 1, Y);
        X := iEnd - 1;
      end;
    ecBackspace:
      if not ReadOnly then
        if X > 0 then
        begin
          // in the middle of line
          if (not PersistentBlocks) and FSelection.IsSelected then
            DoAndCorrectXY(RemoveSelectedBlock)
          else
          begin
            ReLine;
            if BackSpaceUnindents then
              X := GetBackStop(CaretX, CaretY)
            else
              X := CaretX - 1;

            S := Copy(FLines[CaretY], X + 1, CaretX - X);

            { --- UNDO --- }
            if X = CaretX - 1 then
              TJvBackspaceUndo.Create(Self, CaretX, CaretY, S)
            else
              TJvBackspaceUnindentUndo.Create(Self, CaretX, CaretY, S);
            CaretUndo := False;
            { --- /UNDO --- }

            // persistent blocks: adjust selection
            AdjustPersistentBlockSelection(CaretX, CaretY, amDelete, [1]);

            FLines.DeleteText(X, Y, CaretX - 1, Y);

            TextModified(CaretX, CaretY, maDelete, S);
            PaintLine(Y, -1, -1);
          end;
          Changed;
        end
        else
        if Y > 0 then
        begin
          // at the start of line
          if FSelection.IsSelected then
          begin
            BeginCompound;
            try
              DoAndCorrectXY(RemoveSelectedBlock);
              ReLine;
            finally
              EndCompound;
            end;
          end
          else
          begin
            LockUpdate;
            try
              X := Length(FLines[Y - 1]);

              { --- UNDO --- }
              TJvBackspaceUndo.Create(Self, X + 1, CaretY - 1, Lf);
              CaretUndo := False;
              { --- /UNDO --- }

             // persistent blocks: adjust selection
              AdjustPersistentBlockSelection(CaretX, CaretY, amLineConcat, [X, CaretY - 1]);

              FLines.DeleteText(X, Y - 1, -1, Y);
              Dec(Y);
            finally
              UnlockUpdate;
            end;
            UpdateEditorSize;
            TextModified(X, Y, maDelete, sLineBreakStr);
            Invalidate;
            Changed;
          end;
        end
        else
        if not PersistentBlocks and FSelection.IsSelected then
          DoCommand(ecDelete, X, Y, CaretUndo);
    ecDelete:
      if not ReadOnly then
      begin
        LockUpdate;
        try
          if FLines.Count = 0 then
            FLines.Add('');
        finally
          UnlockUpdate;
        end;
        if not PersistentBlocks and FSelection.IsSelected then
          DoAndCorrectXY(RemoveSelectedBlock)
        else
        if X < Length(FLines[Y]) then
        begin
          //{ inside line - â ñåðåäèíå ñòðîêè}
          { --- UNDO --- }
          TJvDeleteUndo.Create(Self, CaretX, CaretY, FLines[Y][X + 1]);
          CaretUndo := False;
          { --- /UNDO --- }

          // persistent blocks: adjust selection (before DeleteText)
          AdjustPersistentBlockSelection(CaretX + 1, CaretY, amDelete, [1]);

          S := FLines[Y][X + 1];
          FLines.DeleteText(X, Y, X, Y);

          TextModified(CaretX, CaretY, maDelete, S);
          PaintLine(CaretY, -1, -1);
          Changed;
        end
        else
        if (Y >= 0) and (Y <= FLines.Count - 2) then
        begin
          //{ at the end of line - â êîíöå ñòðîêè}
          { --- UNDO --- }
          TJvDeleteUndo.Create(Self, CaretX, CaretY, sLineBreakStr);
          CaretUndo := False;
          { --- /UNDO --- }
          // persistent blocks: adjust selection (before DeleteText)
          AdjustPersistentBlockSelection(0, CaretY + 1, amLineConcat, [CaretX, CaretY]);

          FLines.DeleteText(X, Y, -1, Y + 1);

          UpdateEditorSize;
          TextModified(CaretX, CaretY, maDelete, sLineBreakStr);
          Invalidate;
          Changed;
        end;
      end;
    ecTab, ecBackTab:
      begin
        X := GetTabStop(CaretX, CaretY, ACommand = ecTab);
        if not ReadOnly then
        begin
          if FSelection.IsSelected then
            if (ACommand = ecTab) and InsertMode then
              DeleteSelected;
          ReLine;
          if (ACommand = ecTab) and InsertMode then
          begin
            S := FLines[CaretY];
            S2 := SpacesW(X - CaretX);
            { --- UNDO --- }
            TJvInsertTabUndo.Create(Self, CaretX, CaretY, S2);
            CaretUndo := False;
            { --- /UNDO --- }
            FLines.InsertText(CaretX, CaretY, S2);

            TextModified(CaretX, CaretY, maInsert, S2);
            PaintLine(CaretY, -1, -1);
            Changed;
          end;
        end;
        { else }
        { move cursor - oh yes!, it's already moved: X := GetTabStop(..); }
      end;
    ecDeleteLine:
      if not ReadOnly then
      begin
        if (CaretY >= 0) and (CaretY < FLines.Count) then
        begin
          S := FLines[CaretY];
          if (CaretY >= FLines.Count - 1) and (S = '') then
            Exit;

          LockUpdate;
          try
            { --- UNDO --- }
            TJvDeleteLineUndo.Create(Self, CaretX, CaretY, S, CaretY >= FLines.Count - 1);
            { --- /UNDO --- }
            if CaretY < FLines.Count - 1 then
              FLines.Delete(CaretY)
            else
              FLines[CaretY] := '';
            SetCaretInternal(0, CaretY); // set caret to 0/Y when in last line
          finally
            UnlockUpdate;
          end;
          AdjustPersistentBlockSelection(CaretX, CaretY, amDeleteLine, []);
          TextModified(0, CaretY, maDelete, S);
          Invalidate;
          Changed;
        end;
        Exit;
      end;
    ecToUpperCase:
      if not ReadOnly then
        SelText := AnsiUpperCase(SelText);
    ecToLowerCase:
      if not ReadOnly then
        SelText := AnsiLowerCase(SelText);
    ecChangeCase:
      if not ReadOnly then
        SelText := AnsiChangeCase(SelText);
  end;
  Result := False;
end;

function TJvCustomWideEditor.GetSelText: WideString;
var
  S: WideString;
  I: Integer;
  Len, CLen: Integer;
  P: PWideChar;
begin
  with FSelection do
  begin
    Len := GetSelLength; // memory size to allocate
    Result := '';
    if Len = 0 then
      Exit;
    SetLength(Result, Len);

    if SelBlockFormat = bfColumn then
    begin
      if Len > 0 then
      begin
        P := Pointer(Result);
        for I := SelBegY to SelEndY do
        begin
          S := FLines[I];
          CLen := Length(S) - SelBegX;
          if CLen < 0 then
            CLen := 0;
          if CLen > SelEndX - SelBegX + 1 then
            CLen := SelEndX - SelBegX + 1;
          if CLen <> 0 then
          begin
            MoveWideChar(S[SelBegX + 1], P^, CLen);
            Inc(P, CLen);
          end;

          if I < SelEndY then
          begin
            MoveWideChar(sLineBreakStr[1], P^, sLineBreakLen);
            Inc(P, sLineBreakLen);
          end;
        end;
      end;
    end
    else
    begin
      if SelBegY = SelEndY then
        MoveWideChar(FLines[SelEndY][SelBegX + 1], Result[1], Len)
      else
      begin
        P := PWideChar(Result);

        // first line
        S := FLines[SelBegY];
        CLen := Length(S) - SelBegX;
        if CLen > 0 then
        begin
          MoveWideChar(S[SelBegX + 1], P^, CLen);
          Inc(P, CLen);
        end;

        // line break
        MoveWideChar(sLineBreakStr[1], P^, sLineBreakLen);
        Inc(P, sLineBreakLen);

        // lines between
        for I := SelBegY + 1 to SelEndY - 1 do
        begin
         // line
          S := FLines[I];
          if S <> '' then
          begin
            MoveWideChar(S[1], P^, Length(S));
            Inc(P, Length(S));
          end;

          // line break
          MoveWideChar(sLineBreakStr[1], P^, sLineBreakLen);
          Inc(P, sLineBreakLen);
        end;

        // last line
        S := FLines[SelEndY];
        CLen := SelEndX + Ord(SelBlockFormat = bfInclusive);
        if CLen > Length(S) then
          CLen := Length(S);
        if CLen > 0 then
          MoveWideChar(S[1], P^, CLen);
      end;
    end;
  end;
end;

procedure TJvCustomWideEditor.SetSelText(const AValue: WideString);
begin
  BeginUpdate;
  BeginCompound;
  try
    with FSelection do
    begin
      if IsSelected then
        DeleteSelected
      else
      begin
        SelBegX := CaretX;
        SelBegY := CaretY;
      end;
      if FSelection.SelBlockFormat = bfColumn then
        InsertColumnText(FSelection.SelBegX, FSelection.SelBegY, AValue)
      else
        InsertText(AValue);

      IsSelected := Length(AValue) > 0;
      Selecting := False;
      GetEndPosCaretW(AValue, SelBegX, SelBegY, SelEndX, SelEndY);
      if IsSelected then
        Inc(SelEndX);
      SetSelUpdateRegion(SelBegY, SelEndY);
    end;
  finally
    EndCompound;
    EndUpdate;
  end;
end;

function TJvCustomWideEditor.GetText: string;
begin
  Result := FLines.Text;
end;

procedure TJvCustomWideEditor.ClipboardCopy;
begin
  // Set both clipboard formats with one clipboard lock, otherwise clipboard viewers may have
  // a hard time and even mess up the clipboard content (like mRemote).
  Clipboard.Open;
  try
    Clipboard.AsText := GetSelText;
    SetClipboardBlockFormat(SelBlockFormat);
  finally
    Clipboard.Close;
  end;
end;

procedure TJvCustomWideEditor.InsertText(const Text: WideString);
var
  X, Y: Integer;
begin
  PaintCaret(False);
  try
    { --- UNDO --- }
    TJvInsertUndo.Create(Self, CaretX, CaretY, Text);
    { --- /UNDO --- }
    FLines.InsertText(CaretX, CaretY, Text);
    TextModified(CaretX, CaretY, maInsert, Text);

    GetEndPosCaretW(Text, CaretX, CaretY, X, Y); // get new caret position
    SetCaretInternal(X + 1, Y);

    Changed;
  finally
    PaintCaret(True);
  end;
end;

procedure TJvCustomWideEditor.InsertColumnText(X, Y: Integer; const Text: WideString);
begin
  if X < 0 then
    X := 0;
  if Y < 0 then
    Y := 0;
  { --- UNDO --- }
  TJvInsertColumnUndo.Create(Self, X, Y, Text);
  { --- /UNDO --- }
  FLines.InsertColumnText(X, Y, Text);
  TextModified(X, Y, maInsertColumn, Text);
end;

// Substitutes a word in a cursor position on NewString
// WideString NewString should not contain Cr, Lf [translated]

procedure TJvCustomWideEditor.ReplaceWord(const NewString: WideString);
var
  iBeg, iEnd: Integer;
  S, W: WideString;
  X: Integer;
begin
  BeginUpdate;
  PaintCaret(False);
  try
    S := FLines[CaretY];
    while CaretX > Length(S) do
      S := S + ' ';
    W := TrimW(GetWordOnPos2W(S, CaretX, iBeg, iEnd));
    if W = '' then
    begin
      iBeg := CaretX + 1;
      iEnd := CaretX
    end;
    { --- UNDO --- }
    NotUndoable;
    //TJvReplaceUndo.Create(Self, CaretX, CaretY, iBegSX - 1, CaretY, W, NewString);
    { --- /UNDO --- }
    Delete(S, iBeg, iEnd - iBeg);
    Insert(NewString, S, iBeg);
    FLines.Internal[CaretY] := S;
    X := iBeg + Length(NewString) - 1;
    TextModified(CaretX, CaretY, maInsert, NewString);
    PaintLine(CaretY, -1, -1);
    SetCaretInternal(X, CaretY);
    Changed;
  finally
    PaintCaret(True);
    EndUpdate;
  end;
end;

{ Substitutes a word on the cursor position by NewString [translated] }

procedure TJvCustomWideEditor.ReplaceWord2(const NewString: WideString);
var
  S, W: WideString;
  iBegSX, iEndSX: Integer; { [1..Length] }
  X, Y: Integer;
begin
  S := '';
  if CaretY < FLines.Count then
    S := FLines[CaretY];

  W := TrimW(GetWordOnPosExW(S, CaretX + 1, iBegSX, iEndSX));
  if W <> NewString then
  begin
    PaintCaret(False);
    try
      BeginCompound;
      try
        ReLine;
        if Length(W) = 0 then
        begin
          iBegSX := CaretX + 1;
          iEndSX := CaretX;
        end;
        { --- UNDO --- }
        TJvReplaceUndo.Create(Self, CaretX, CaretY, iBegSX - 1, CaretY, W, NewString);
        { --- /UNDO --- }

        if iBegSX <= iEndSX then
          FLines.DeleteText(iBegSX - 1, CaretY, iEndSX - 1, CaretY);
        FLines.InsertText(iBegSX - 1, CaretY, NewString);
        TextModified(iBegSX - 1, CaretY, maReplace, NewString);

        GetEndPosCaretW(NewString, iBegSX - 1, CaretY, X, Y); // get end caret position
        SetCaretInternal(X + 1, Y);
      finally
        EndCompound;
      end;
      Changed;
    finally
      PaintCaret(True);
    end;
  end;
end;

procedure TJvCustomWideEditor.IndentColumns(X, BegY, EndY: Integer);
var
  Y: Integer;
  S: WideString;
begin
  if BegY < 0 then
    BegY := 0;
  if BegY >= FLines.Count then
    BegY := FLines.Count - 1;
  if EndY < 0 then
    EndY := 0;
  if EndY >= FLines.Count then
    EndY := FLines.Count - 1;
  if EndY < BegY then
    Exit;
  if X < 0 then
    X := 0;

  S := SpacesW(2);
  for Y := BegY to EndY - 1 do
    S := S + sLineBreakStr + SpacesW(2);

  InsertColumnText(X, BegY, S);

  Changed;
  if UpdateLock = 0 then
    Invalidate;
end;

procedure TJvCustomWideEditor.UnIndentColumns(X: Integer; BegY, EndY: Integer);
var
  S, UnindentedText: WideString;
  Y: Integer;
  Len, L: Integer;
begin
  if BegY < 0 then
    BegY := 0;
  if BegY >= FLines.Count then
    BegY := FLines.Count - 1;
  if EndY < 0 then
    EndY := 0;
  if EndY >= FLines.Count then
    EndY := FLines.Count - 1;
  if EndY < BegY then
    Exit;
  if X < 0 then
    X := 0;

  Inc(X); // for WideString operations

  LockUpdate;
  try
    UnindentedText := '';
    for Y := BegY to EndY do
    begin
      S := FLines[Y];
      Len := Length(S);

      // how many spaces to delete
      L := 0;
      while (X + L <= Len) and (L < 2) and (S[X + L] = ' ') do
        Inc(L);

      if L > 0 then
      begin
        UnindentedText := UnindentedText + SpacesW(L);
        Delete(S, X, L);
        FLines.Internal[Y] := S;
      end;
      if Y < EndY then
        UnindentedText := UnindentedText + sLineBreakStr;
    end;
  finally
    UnlockUpdate;
  end;

  Dec(X); // for caret operations
  if Length(UnindentedText) > 0 then
  begin
    { --- UNDO --- }
    TJvUnindentColumnUndo.Create(Self, CaretX, CaretY, X, BegY, UnindentedText);
    { --- /UNDO --- }
    TextModified(X, BegY, maDelete, UnindentedText);

    Changed;
    if UpdateLock = 0 then
      Invalidate;
  end;
end;

procedure TJvCustomWideEditor.ClipboardPaste;
var
  ClipS: string;
  X, Y, EndX, EndY: Integer;
begin
  if (CaretY > FLines.Count - 1) and (FLines.Count > 0) then
    if BeepOnError then
      Beep;
  ClipS := Clipboard.AsText;
  if ClipS = '' then
    Exit;
  ClipS := ExpandTabs(AdjustLineBreaks(ClipS));

  BeginUpdate;
  try
    PaintCaret(False);

    ReLine;
    with FSelection do
    begin
      X := CaretX;
      Y := CaretY;
      BeginCompound;
      try
        if IsSelected then
        begin
          if BlockOverwrite and not PersistentBlocks then
          begin
            X := SelBegX;
            Y := SelBegY;
          end;
          RemoveSelectedBlock;
        end;
        if FLines.Count > 0 then
          ReLine;

       SelBlockFormat := GetClipboardBlockFormat;
       if SelBlockFormat in [bfInclusive, bfNonInclusive, bfLine] then
        begin
          // special line block mode handling
          if SelBlockFormat = bfLine then
          begin
            X := 0;
            if (ClipS = '') or (ClipS[Length(ClipS)] <> Lf) then
              ClipS := ClipS + sLineBreakStr;
          end;

          { --- UNDO --- }
          TJvInsertUndo.Create(Self, X, Y, ClipS);
          { --- /UNDO --- }

          FLines.InsertText(X, Y, ClipS);
          TextModified(X, Y, maInsert, ClipS);

          // get new caret position
          GetEndPosCaretW(ClipS, X, Y, EndX, EndY);
          Inc(EndX);

          if PersistentBlocks then
          begin
            SelBegX := X;
            SelBegY := Y;
            // special line block mode handling
            if SelBlockFormat = bfLine then
            begin
              Dec(EndY);
              SelEndX := Max_X;
            end
            else
              SelEndX := EndX;

            SelEndY := EndY;
            IsSelected := True;
            Selecting := False;
            SetSelUpdateRegion(SelBegY, SelEndY);
          end;
          X := EndX;
          Y := EndY;
        end
        else
        if SelBlockFormat = bfColumn then
        begin
          InsertColumnText(X, Y, ClipS);
          GetEndPosCaretW(ClipS, X, Y, X, Y);
          X := CaretX - 1;
          Inc(X);
        end;
      finally
        EndCompound;
      end;
    end; // with

    SetCaretInternal(X, Y);

    Changed;
  finally
    PaintCaret(True);
    EndUpdate; {!!! Causes copying all [translated] }
  end;
end;

procedure TJvCustomWideEditor.DeleteSelected;
var
  S: WideString;
  X, Y: Integer;
begin
  with FSelection do
  begin
    X := SelBegX;
    Y := SelBegY;
    if IsSelected then
    begin
      BeginUpdate;
      PaintCaret(False);
      try
        S := GetSelText;
        {--- UNDO ---}
        TJvDeleteSelectedUndo.Create(Self, CaretX, CaretY, S);
        {--- /UNDO ---}
        IsSelected := False;
        Selecting := False;
        if SelBlockFormat in [bfInclusive, bfNonInclusive, bfLine] then
        begin
          FLines.DeleteText(X, Y, SelEndX - 1 + Ord(SelBlockFormat = bfInclusive), SelEndY);
          TextModified(SelBegX, SelBegY, maDelete, S);
        end
        else
        if SelBlockFormat = bfColumn then
        begin
          Y := CaretY;
          FLines.DeleteColumnText(SelBegX, SelBegY, SelEndX, SelEndY);
          TextModified(SelBegX, SelBegY, maDeleteColumn, S);
        end;
        SetCaretInternal(X, Y);
        Changed;
      finally
        PaintCaret(True);
        EndUpdate;
      end;
    end;
  end;
end;

procedure TJvCustomWideEditor.Clear;
begin
  FLines.Clear;
end;

function TJvCustomWideEditor.GetLines: TWStrings;
begin
  Result := FLines;
end;

procedure TJvCustomWideEditor.SetLines(ALines: TWStrings);
begin
  if ALines <> nil then
    FLines.Assign(ALines);
  {--- UNDO ---}
  NotUndoable;
  {--- /UNDO ---}
end;

procedure TJvCustomWideEditor.TextAllChangedInternal(Unselect: Boolean);
begin
  inherited TextAllChangedInternal(Unselect);
  TextModified(0, 0, maAll, '');
  UpdateEditorView;
end;

function TJvCustomWideEditor.ExpandTabs(const S: WideString): WideString;
var
  ps, I: Integer;
  Sp: WideString;
  Tabs, LenSp: Integer;
  P: PWideChar;
begin
  ps := Pos(Tab, S);
  if ps > 0 then
  begin
    // How many Tab chars?
    Tabs := 1;
    for I := ps + 1 to Length(S) do
      if S[I] = Tab then
        Inc(Tabs);

    Sp := SpacesW(GetDefTabStop(0, True));
    LenSp := Length(Sp);

    // needed memory
    SetLength(Result, Length(S) - Tabs + Tabs * LenSp);
    P := PWideChar(Result);

    // copy the chars before the Tab
    if ps > 1 then
    begin
      MoveWideChar(S[1], P[0], ps);
      Inc(P, ps - 1);
    end;

    for I := ps to Length(S) do
    begin
      if S[I] <> Tab then
      begin
        P[0] := S[I];
        Inc(P);
      end
      else
      if LenSp > 0 then
      begin
        MoveWideChar(Sp[1], P[0], LenSp);
        Inc(P, LenSp);
      end;
    end;
  end
  else
    Result := S;
end;

procedure TJvCustomWideEditor.TextModified(ACaretX, ACaretY: Integer; Action: TModifiedAction; const Text: WideString);
begin
end;

function TJvCustomWideEditor.GetLineCount: Integer;
begin
  Result := FLines.Count;
end;

function TJvCustomWideEditor.GetLineLength(Index: Integer): Integer;
begin
  Result := Length(FLines[Index]);
end;

function TJvCustomWideEditor.FindNotBlankCharPosInLine(Line: Integer): Integer;
begin
  Result := FindNotBlankCharPos(FLines[Line]);
end;

procedure TJvCustomWideEditor.SetLockText(const Text: WideString);
begin
  FLines.SetLockText(Text);
end;

function TJvCustomWideEditor.GetWordOnCaret: WideString;
begin
  if CaretY < FLines.Count then
    Result := GetWordOnPos(FLines[CaretY], CaretX + 1)
  else
    Result := '';
end;

function TJvCustomWideEditor.GetAutoIndentStop(Y: Integer): Integer;
var
  I, Len: Integer;
  S: WideString;
begin
  Result := 0;

 // find non-empty line
  Dec(Y);
  while Y > 0 do
  begin
    S := FLines[Y];
    if Length(TrimW(S)) > 0 then
      Break;
    Dec(Y);
  end;
  if Y < 0 then
    Exit;

  Len := Length(S);
  I := 1;
  while (I <= Len) and (S[I] = ' ') do
    Inc(I);
  Result := I - 1;
end;

function TJvCustomWideEditor.GetTabStop(X, Y: Integer; Next: Boolean): Integer;

  procedure UpdateTabStops;
  var
    S: WideString;
    J, I: Integer;
    Len, TabPosLen: Integer;
  begin
    TabPosLen := Max_X;
    if Length(FTabPos) <> Max_X then
    begin
      SetLength(FTabPos, Max_X);
      TabPosLen := Max_X;
    end;
    FillChar(FTabPos[0], TabPosLen * SizeOf(FTabPos[0]), False);
    if SmartTab then
    begin
      J := 1;
      I := 1;
      while Y - J >= 0 do
      begin
        S := TrimRightW(FLines[Y - J]);
        Len := Length(S);
        if Len > TabPosLen then
        begin
          SetLength(FTabPos, Len);
          FillChar(FTabPos[TabPosLen], (TabPosLen - Len) * SizeOf(FTabPos[0]), False);
        end;
        if Len > I then
          FTabPos[Len - 1] := True;
        while I <= Len do
        begin
          if CharInSetW(S[I], IdentifierSymbols) then
          begin
            FTabPos[I - 1] := True;
            while (I <= Len) and CharInSetW(S[I], IdentifierSymbols) do
              Inc(I);
          end;
          Inc(I);
        end; { for }

        if I >= Max_X_Scroll then
          Break;
        if J >= VisibleRowCount * 2 then
          Break;
        Inc(J);
      end;
    end;
  end;

var
  I: Integer;
begin
  UpdateTabStops;
  Result := X;
  if Next then
  begin
    for I := X + 1 to Length(FTabPos) - 1 do
      if FTabPos[I] then
      begin
        Result := I;
        Exit;
      end;
    if Result = X then
      Result := GetDefTabStop(X, True);
  end
  else
  begin
    if Result = X then
      Result := GetDefTabStop(X, False);
  end;
end;

function TJvCustomWideEditor.GetBackStop(X, Y: Integer): Integer;

  procedure UpdateBackStops;
  var
    S: WideString;
    J, I, K: Integer;
    Len, TabPosLen: Integer;
  begin
    J := 1;
    I := X - 1;
    TabPosLen := Max_X;
    if Length(FTabPos) <> Max_X then
    begin
      SetLength(FTabPos, Max_X);
      TabPosLen := Max_X;
    end;
    FillChar(FTabPos[0], TabPosLen * SizeOf(FTabPos[0]), False);
    FTabPos[0] := True;
    while Y - J >= 0 do
    begin
      S := FLines[Y - J];
      Len := Length(S);
      if Len > TabPosLen then
      begin
        SetLength(FTabPos, Len);
        FillChar(FTabPos[TabPosLen], (TabPosLen - Len) * SizeOf(FTabPos[0]), False);
      end;

      for K := 1 to Min(Len, I) do
        if S[K] <> ' ' then
        begin
          I := K;
          FTabPos[I - 1] := True;
          Break;
        end;
      if I = 1 then
        Break;
      if J >= VisibleRowCount * 2 then
        Break;
      Inc(J);
    end;
  end;

var
  I: Integer;
  S: WideString;
begin
  Result := X - 1;
  S := TrimRightW(FLines[Y]);
  if (TrimW(Copy(S, 1, X)) = '') and
    ((X + 1 > Length(S)) or (S[X + 1] <> ' ')) then
  begin
    UpdateBackStops;
    for I := X downto 0 do
      if FTabPos[I] then
      begin
        Result := I;
        Exit;
      end;
  end;
end;

//=== { TJvInsertUndo } ======================================================

constructor TJvInsertUndo.Create(AJvEditor: TJvCustomWideEditor;
  ACaretX, ACaretY: Integer; const AText: WideString);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY);
  FText := AText;
  if JvEditor.PersistentBlocks then
    SaveSelection;
end;

function TJvInsertUndo.GetEditor: TJvCustomWideEditor;
begin
  Result := TJvCustomWideEditor(FJvEditor);
end;

procedure TJvInsertUndo.Undo;
var
  Text: WideString;
  EndX, EndY: Integer;
  du: TJvInsertUndo;
begin
  Text := '';
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := TJvInsertUndo(LastUndo).FText + Text;
      Dec(FPtr);
      if not JvEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);

    du := TJvInsertUndo(Items[FPtr]);
  end;

  GetEndPosCaretW(Text, du.CaretX, du.CaretY, EndX, EndY); // get end caret position
  TJvCustomWideEditor(JvEditor).FLines.DeleteText(du.CaretX, du.CaretY, EndX, EndY);
  TJvCustomWideEditor(JvEditor).TextModified(du.CaretX, du.CaretY, maDelete, Text);

  TJvCustomWideEditor(JvEditor).SetCaretInternal(du.CaretX, du.CaretY);
end;

//=== { TJvOverwriteUndo } ===================================================

constructor TJvOverwriteUndo.Create(AJvEditor: TJvCustomWideEditor;
  ACaretX, ACaretY: Integer; const AOldText, ANewText: WideString);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY);
  FOldText := AOldText;
  FNewText := ANewText;
end;

function TJvOverwriteUndo.GetEditor: TJvCustomWideEditor;
begin
  Result := TJvCustomWideEditor(FJvEditor);
end;

procedure TJvOverwriteUndo.Undo;
var
  OldText, NewText: WideString;
  EndX, EndY: Integer;
  OverwriteUndo: TJvOverwriteUndo;
begin
  OldText := '';
  NewText := '';
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      OldText := TJvOverwriteUndo(LastUndo).FOldText + OldText;
      NewText := TJvOverwriteUndo(LastUndo).FNewText + NewText;
      Dec(FPtr);
      if not GetEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);
    OverwriteUndo := TJvOverwriteUndo(Items[FPtr]);
  end;
  with OverwriteUndo do
  begin
    GetEndPosCaretW(NewText, CaretX, CaretY, EndX, EndY); // get end caret position
    GetEditor.FLines.DeleteText(CaretX, CaretY, EndX, EndY);
    GetEditor.FLines.InsertText(CaretX, CaretY, OldText);
    GetEditor.TextModified(CaretX, CaretY, maReplace, OldText);

    GetEditor.SetCaretInternal(CaretX, CaretY);
  end;
end;

//=== { TJvInsertColumnUndo } ================================================

procedure TJvInsertColumnUndo.Undo;
var
  I: Integer;
  SS: TWStringList;
  S: WideString;
begin
  { Do not call GetEditor.FLines.DeleteColumnText() here because it has not
    the functionality needed in this context. It deletes the columns from
    [BegX..EndX] even if the inserted line was not as long as EndX-BegX+1. }

  SS := TWStringList.Create;
  try
    SS.Text := FText;
    for I := 0 to SS.Count - 1 do
    begin
      S := GetEditor.FLines[CaretY + I];
      Delete(S, CaretX + 1, Length(SS[I]));
      GetEditor.FLines.Internal[CaretY + I] := S;
    end;
  finally
    SS.Free;
  end;
  GetEditor.TextModified(CaretX, CaretY, maDelete, FText);

  GetEditor.SetCaretInternal(CaretX, CaretY);
end;

//=== { TJvUnindentColumnUndo } ==============================================

constructor TJvUnindentColumnUndo.Create(AJvEditor: TJvCustomWideEditor;
  ACaretX, ACaretY, ABegX, ABegY: Integer; const AText: WideString);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY, AText);
  SaveSelection;
  FBegX := ABegX;
  FBegY := ABegY;
end;

procedure TJvUnindentColumnUndo.Undo;
var
  BegX, BegY: Integer;
begin
  BegX := FBegX;
  BegY := FBegY;
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      with TJvUnindentColumnUndo(LastUndo) do
      begin
        GetEditor.FLines.InsertColumnText(FBegX, FBegY, FText);
        if BegX > FBegX then
          BegX := FBegX;
        if BegY > FBegY then
          BegY := FBegY;
      end;
      Dec(FPtr);
      if not GetEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);
  end;
  GetEditor.TextModified(BegX, BegY, maInsert, GetEditor.FLines[BegY]);

  RestoreSelection;
  with TJvUnindentColumnUndo(TJvUndoBufferAccessProtected(UndoBuffer).LastUndo) do
    GetEditor.SetCaretInternal(CaretX, CaretY);
end;

//=== { TJvDeleteUndo } ======================================================

procedure TJvDeleteUndo.Undo;
var
  Text: WideString;
begin
  Text := '';
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := TJvDeleteUndo(LastUndo).FText + Text;
      Dec(FPtr);
      if not GetEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);

    with TJvDeleteUndo(Items[FPtr]) do
    begin
      GetEditor.FLines.InsertText(CaretX, CaretY, Text);
      GetEditor.TextModified(CaretX, CaretY, maInsert, Text);

      GetEditor.SetCaretInternal(CaretX, CaretY);
    end;
  end;
end;


//=== { TJvDeleteLineUndo } ==================================================

{procedure TJvDeleteLineUndo.Redo;
begin
  GetEditor.FLines.Insert(CaretY, FText);
end;}

constructor TJvDeleteLineUndo.Create(AJvEditor: TJvCustomWideEditor; ACaretX, ACaretY: Integer;
  const AText: WideString; ALastLineDelete: Boolean);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY, AText);
  FLastLineDelete := ALastLineDelete;
end;

procedure TJvDeleteLineUndo.Undo;
begin
  GetEditor.LockUpdate;
  try
    if FLastLineDelete then
    begin
      GetEditor.FLines[CaretY] := FText;
      GetEditor.TextModified(CaretX, CaretY, maReplace, FText);
    end
    else
    begin
      GetEditor.FLines.Insert(CaretY, FText);
      GetEditor.TextModified(CaretX, CaretY, maInsert, FText);
    end;
  finally
    GetEditor.UnlockUpdate;
  end;
  GetEditor.SetCaretInternal(CaretX, CaretY);
end;

//=== { TJvBackspaceUndo } ===================================================

procedure TJvBackspaceUndo.Undo;
var
  Text: WideString;
  StartPtr: Integer;
begin
  Text := '';
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    StartPtr := FPtr;
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := Text + TJvDeleteUndo(LastUndo).FText;
      Dec(FPtr);
      if not GetEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);
  end;

  with TJvDeleteUndo(UndoBuffer.Items[StartPtr]) do
  begin
    GetEditor.FLines.InsertText(CaretX - 1, CaretY, Text);
    GetEditor.TextModified(CaretX - 1, CaretY, maInsert, Text);
  end;

  // set caret on last backspace undo's position
  with TJvDeleteUndo(UndoBuffer.Items[TJvUndoBufferAccessProtected(UndoBuffer).FPtr]) do
    if (FText = Lf) or (FText = Cr) then // a line was removed by backspace
      GetEditor.SetCaretInternal(0, CaretY + 1)
    else
      GetEditor.SetCaretInternal(CaretX, CaretY);
end;

procedure TJvBackspaceUnindentUndo.Undo;
var
  Text: WideString;
begin
  Text := '';
  with TJvUndoBufferAccessProtected(UndoBuffer) do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := Text + TJvDeleteUndo(LastUndo).FText;
      Dec(FPtr);
      if not GetEditor.GroupUndo then
        Break;
    end;
    Inc(FPtr);

    with TJvDeleteUndo(Items[FPtr]) do
    begin
      GetEditor.FLines.InsertText(CaretX - Length(Text), CaretY, Text);
      GetEditor.TextModified(CaretX - Length(Text), CaretY, maInsert, Text);
      // set caret on last backspace undo's position
      GetEditor.SetCaretInternal(CaretX, CaretY);
    end;
  end;
end;

//=== { TJvReplaceUndo } =====================================================

constructor TJvReplaceUndo.Create(AJvEditor: TJvCustomWideEditor;
  ACaretX, ACaretY: Integer; ABegX, ABegY: Integer; const AText, ANewText: WideString);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY);
  FBegX := ABegX;
  FBegY := ABegY;
  FText := AText;
  FNewText := ANewText;
end;

function TJvReplaceUndo.GetEditor: TJvCustomWideEditor;
begin
  Result := TJvCustomWideEditor(FJvEditor);
end;

procedure TJvReplaceUndo.Undo;
var
  EndX, EndY: Integer;
begin
  GetEndPosCaretW(FNewText, FBegX, FBegY, EndX, EndY);
  GetEditor.FLines.DeleteText(FBegX, FBegY, EndX, EndY);
  GetEditor.FLines.InsertText(FBegX, FBegY, FText);
  GetEditor.TextModified(FBegX, FBegY, maReplace, FText);

  GetEditor.SetCaretInternal(CaretX, CaretY);
end;

//=== { TJvDeleteSelectedUndo } ==============================================

constructor TJvDeleteSelectedUndo.Create(AJvEditor: TJvCustomWideEditor;
  ACaretX, ACaretY: Integer; const AText: WideString);
begin
  inherited Create(AJvEditor, ACaretX, ACaretY, AText);
  SaveSelection;
end;

procedure TJvDeleteSelectedUndo.Undo;
var
  S: WideString;
  I: Integer;
begin
  with FSelection^ do
  begin
    if SelBlockFormat in [bfInclusive, bfNonInclusive, bfLine] then
    begin
      GetEditor.FLines.InsertText(SelBegX, SelBegY, FText);
      GetEditor.TextModified(SelBegX, SelBegY, maInsert, FText);
    end
    else
    if SelBlockFormat = bfColumn then
    begin
      for I := SelBegY to SelEndY do
      begin
        S := GetEditor.FLines[I];
        Insert(SubStrBySeparatorW(FText, I - SelBegY, sLineBreakStr), S, SelBegX + 1);
        GetEditor.FLines.Internal[I] := S;
      end;
      GetEditor.TextModified(SelBegX, SelBegY, maInsertColumn, FText);
    end;

    RestoreSelection;
    GetEditor.SetCaretInternal(CaretX, CaretY);
  end; // with
end;

//=== { TJvEditorCompletion } ================================================

constructor TJvWideCompletion.Create(AJvEditor: TJvCustomWideEditor);
begin
  inherited Create(AJvEditor);
  FIdentifiers := TWStringList.Create;
  FTemplates := TWStringList.Create;
  FCaretChar := '|';
  FCRLF := '/n';
  FSeparator := '=';
end;

destructor TJvWideCompletion.Destroy;
begin
  inherited Destroy;
  FIdentifiers.Free;
  FTemplates.Free;
end;

{ Substitutes word on the cursor position by NewString [translated] }

procedure TJvWideCompletion.ReplaceWord(const NewString: WideString);
var
  S, W: WideString;
  X, Y: Integer;
  iBegSX, iEndSX: Integer;
  NewCaret: Integer;
begin
  with TJvCustomWideEditor(JvEditor) do
  begin
    if CaretY < FLines.Count then
      S := FLines[CaretY];
    W := GetNextWordPosExW(S, CaretX, iBegSX, iEndSX);
    if W <> NewString then
    begin
      BeginUpdate;
      PaintCaret(False);
      try
        BeginCompound;
        try
          Deselect;
          ReLine;

          if Length(W) = 0 then
          begin
            iBegSX := CaretX + 1;
            iEndSX := CaretX;
          end;
          case Mode of
            cmIdentifiers:
              begin
                S := NewString;
                if Assigned(FOnCompletionApply) then
                  FOnCompletionApply(Self, W, S);
                NewCaret := -1;
              end;
            cmTemplates:
              begin
                S := ReplaceString(NewString, FCRLF, sLineBreakStr + SpacesW(CaretX -
                  Length(W)));
                S := ReplaceString(S, FCaretChar, '');
                NewCaret := Pos(FCaretChar, ReplaceString(NewString, FCRLF, sLineBreakStr)) - 1;
              end;
          else
            raise EJvEditorError.CreateRes(@RsEInvalidCompletionMode);
          end;
          {--- UNDO ---}
          TJvReplaceUndo.Create(TJvCustomWideEditor(JvEditor), CaretX, CaretY,
            iBegSX - 1, CaretY, W, S);
          {--- /UNDO ---}
          if iBegSX <= iEndSX then
            FLines.DeleteText(iBegSX - 1, CaretY, iEndSX - 1, CaretY);
          FLines.InsertText(iBegSX - 1, CaretY, S);
          TextModified(iBegSX - 1, CaretY, maReplace, S);

          if NewCaret >= 0 then
            SetLength(S, NewCaret); // truncate S to the new caret position
          GetEndPosCaretW(S, iBegSX - 1, CaretY, X, Y);
          SetCaretInternal(X + 1, Y);
        finally
          EndCompound;
        end;
        Changed;
      finally
        PaintCaret(True);
        EndUpdate;
      end;
    end;
  end;
end;

procedure TJvWideCompletion.MakeItems;
var
  I: Integer;
  S: WideString;
begin
  Items.BeginUpdate;
  try
    Items.Clear;
    case Mode of
      cmIdentifiers:
        for I := 0 to FIdentifiers.Count - 1 do
          Items.Add(FIdentifiers[I]);
      cmTemplates:
        begin
          with TJvCustomWideEditor(JvEditor) do
            if FLines.Count > CaretY then
              S := GetWordOnPosW(FLines[CaretY], CaretX)
            else
              S := '';
          for I := 0 to FTemplates.Count - 1 do
            if StrLICompW(PWideChar(FTemplates[I]), PWideChar(S), Length(S)) = 0 then
              Items.Add(FTemplates[I]);
          if Items.Count = 0 then
            Items.Assign(FTemplates);
        end;
    end;
  finally
    Items.EndUpdate;
  end;
end;

procedure TJvWideCompletion.FindSelItem(var Eq: Boolean);
var
  S: WideString;

  function FindFirst(SS: TStrings; S: string): Integer;
  var
    I: Integer;
  begin
    for I := 0 to SS.Count - 1 do
      if StrLIComp(PChar(SS[I]), PChar(S), Length(S)) = 0 then
      begin
        Result := I;
        Exit;
      end;
    Result := -1;
  end;

begin
  with TJvCustomWideEditor(JvEditor) do
    if FLines.Count > 0 then
      S := GetWordOnPosW(FLines[CaretY], CaretX)
    else
      S := '';
  if TrimW(S) = '' then
    ItemIndex := -1
  else
    ItemIndex := FindFirst(Items, S);
  Eq := (ItemIndex > -1) and SameText(TrimW(SubStrBySeparatorW(Items[ItemIndex], 0, FSeparator)), S);
end;

function TJvWideCompletion.GetStrings(Index: Integer): TWStrings;
begin
  case Index of
    0:
      Result := FIdentifiers;
    1:
      Result := FTemplates;
  else
    Result := nil;
  end;
end;

procedure TJvWideCompletion.SetStrings(Index: Integer; AValue: TWStrings);
begin
  case Index of
    0:
      FIdentifiers.Assign(AValue);
    1:
      FTemplates.Assign(AValue);
  end;
end;

function TJvWideCompletion.GetIdentifierCount: Integer;
begin
  Result := FIdentifiers.Count;
end;

function TJvWideCompletion.GetTemplateCount: Integer;
begin
  Result := FTemplates.Count;
end;

procedure TJvWideCompletion.ReplaceWordItemIndex(SubStrStart: Integer);
begin
  ReplaceWord(SubStrBySeparatorW(Items[ItemIndex], SubStrStart, FSeparator));
end;

function TJvCustomWideEditor.GetCompletion: TJvWideCompletion;
begin
  Result := TJvWideCompletion(inherited Completion);
end;

procedure TJvCustomWideEditor.SetCompletion(const Value: TJvWideCompletion);
begin
  inherited Completion := Value;
end;

function TJvWideCompletion.GetSeparator: string;
begin
  Result := FSeparator;
end;

{$IFDEF UNITVERSIONING}
initialization
  RegisterUnitVersion(HInstance, UnitVersioning);

finalization
  UnregisterUnitVersion(HInstance);
{$ENDIF UNITVERSIONING}

end.
