/// Synopse extended TMemo visual component
// - licensed under a MPL/GPL/LGPL tri-license; version 2.26
unit SynMemoEx;

{
    This file is part of Synopse extended TMemo

    Synopse SynMemoEx. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  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

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

  The Original Code is Synopse SynMemoEx.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****


  Based on TMemoEx, by R&A Library 2.03  (c) R&A, 1996-2000.

  R&A Library seems now unsupported. Web site is dead, as is their email address.
  We decided to put this unit, mostly rewritten, under a MPL/GPL/LGPL tri-license.

  * full code rewrite for speed, stability and new features
  * regroup in one unit w/out RA Consts
  * no expandTabs: too slow
  * FAttrs are calculated on the fly: spare RAM on syntax highlight (only text is stored)
  * don't use FLines.text on often-call methods (insertText, deleteSelected...)
  * fClipProtect: clipBoard Copy/Cut limited to 2K for others programs (copyrights)
  * find (text) command
  * a lot of bug fixes

  Version 1.18
  - Unicode compatibility

}

{$I Synopse.inc}

interface

{.$define CLIPBOARDPROTECT} // if defined, ClipProtect truncates clipboard to 2KB
// use only with ONE TMemoEx at once

{$IFNDEF MEMOEX_NOEDITOR}
{$DEFINE MEMOEX_EDITOR}           {if not MEMOEX_EDITOR then mode = Viewer}
{$ENDIF}
{$DEFINE MEMOEX_DEFLAYOUT}        {set default keyboard layout}
{$IFNDEF MEMOEX_NOUNDO}
{$DEFINE MEMOEX_UNDO}             {enable undo}
{$ENDIF}
{$IFNDEF MEMOEX_NOCOMPLETION}
{$DEFINE MEMOEX_COMPLETION}       {enable code completion}
{$ENDIF}

{$IFNDEF MEMOEX_EDITOR}
{$UNDEF MEMOEX_DEFLAYOUT}
{$UNDEF MEMOEX_UNDO}
{$UNDEF MEMOEX_COMPLETION}
{$ENDIF MEMOEX_EDITOR}

{ $D-,L-} // avoid jumping in the source for any EComplete exceptions e.g.

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

uses
  Windows,
  Messages,
  SysUtils,
  Classes,
  Types,
  Graphics,
  Controls,
  Forms,
  ExtCtrls,
  StdCtrls,
  ClipBrd,
  Menus
  {$ifdef FPC}  // FPC compatibility by alf (alfred) - thanks for the patch!
  , LCLType
  {$endif FPC}
  {$ifdef UNICODE}
  , UITypes
  {$endif UNICODE};

const
  RAEditorCompletionChars: set of AnsiChar =
    [#8, '_', '0'..'9', 'A'..'Z', 'a'..'z'];
  Separators: set of AnsiChar =
    [#0, ' ', '-', #13, #10, '.', ',', '/', '\', ':', '+', '%', '*', '(', ')',
    ';', '=', '{', '}', '[', ']',  '|', '!', '@', '"'];
  GutterRightMargin = 2;

  WM_EDITCOMMAND = WM_USER + $101;

  RA_EX_STYLE_DEFAULT = 0;
  RA_CASE_CONVERT_UPPER = 0;
  RA_CASE_CONVERT_LOWER = 1;
  RA_CASE_CONVERT_INVERT = 2;

type
  TRAControlScrollBar95 = class
  private
    FKind: TScrollBarKind;
    FPosition: Integer;
    FMin: Integer;
    FMax: Integer;
    FSmallChange: TScrollBarInc;
    FLargeChange: TScrollBarInc;
    FPage: integer;
    FHandle: hWnd;
    FOnScroll: TScrollEvent;
    // FVisible : boolean;
    procedure SetParam(index, Value: Integer);
    // procedure SetVisible(Value : boolean);
    // procedure SetLargeChange(Value: TScrollBarInc);
  protected
    procedure Scroll(ScrollCode: TScrollCode; var ScrollPos: Integer); dynamic;
  public
    constructor Create;
    procedure SetParams(AMin, AMax, APosition, APage: integer);
    procedure DoScroll(var Message: TWMScroll);
    property Kind: TScrollBarKind read FKind write FKind default sbHorizontal;
    property SmallChange: TScrollBarInc read FSmallChange write FSmallChange default 1;
    property LargeChange: TScrollBarInc read FLargeChange write FLargeChange default 1;
    property Min: Integer index 0 read FMin write SetParam default 0;
    property Max: Integer index 1 read FMax write SetParam default 100;
    property Position: Integer index 2 read FPosition write SetParam default 0;
    property Page: integer index 3 read FPage write SetParam;
    property Handle: hWnd read FHandle write FHandle;
    property OnScroll: TScrollEvent read FOnScroll write FOnScroll;
    // property Visible : boolean read FVisible write SetVisible;
  end;

  TCellRect = record
    Width: integer;
    Height: integer;
  end;

  PLineAttr = ^TLineAttr;
  TLineAttr = packed record
    FC, BC: TColor;
    case integer of
      0:
        (Style: TFontStyles;
        ex_style: byte;
        underlined: boolean);
      1:
        (LastInteger: integer);
  end;

  TCustomMemoEx = class;

  TWordUnderCursor = record // for TOnWordClick
    Text: string;
    Style: integer;
    CaretX, CaretY, ParaIndex, ParaOffset: integer;
    TextStart: integer;
    Shift: TShiftState;
  end;

  TLineAttrs = array of TLineAttr;
  TSelAttrs = array of boolean;

  TOnGetLineAttr = procedure(Sender: TObject; const Line: string; index: integer; const SelAttrs: TSelAttrs; var Attrs: TLineAttrs) of object;
  TOnChangeStatus = TNotifyEvent;
  TOnChangeClipboardState = procedure(Sender: TObject; const CanPaste: boolean) of object;
  TOnWordClick = procedure(Sender: TObject; const Clicked: TWordUnderCursor) of object;
  TOnMouseOver = procedure(Sender: TObject; WordStyle: word; var _Cursor: TCursor) of object;
  TOnBreakLine = procedure(Sender: TObject; const Original: string; var _New: string) of object;
  TOnConcatLine = procedure(Sender: TObject; const Original: string; var _New: string) of object;
  TOnTextInsert = procedure(Sender: TObject; var Text: string) of object;
  TOnCaseConversion = function(Sender: TObject; Conversion: byte; const Text: string): string of object;
  TOnInsertBlock = function(Sender: TObject; var Text: string): boolean of object;
  TOnSaveBlock = procedure(Sender: TObject; const Text: string) of object;
  TOnInsertMacro = function(Sender: TObject; MacroID: integer): string of object;
  TOnBlockOperation = function(Sender: TObject; MacroID: integer; const Text: string): string of object;
  TOnSetCaretPos = procedure(Sender: TObject; CaretX, CaretY: integer) of object;
  TOnClipboardPaste = function(Sender: TObject): boolean of object;
  {$IFDEF MEMOEX_COMPLETION}
  TOnPreprocessCompletion = function(Sender: TObject; const ID, Text: string): string of object;
  {$ENDIF}

  PAutoChangeWord = ^TAutoChangeWord;
  TAutoChangeWord = record
    OldWord, NewWord: string;
  end;

  PParagraph = ^TParagraph;
  TParagraph = record
    FPreCount, FCount: integer; // length(FString) = FStrings[0..FCount-1]
    FStrings: array of string;
    FObject: TObject;
  end;

  TEditorStrings = class(TStrings)
  private
    FMemoEx: TCustomMemoEx;
    FList: array of TParagraph;
    FParaLinesCount, FCount: integer;
    FOnChanging: TNotifyEvent;
    FOnAfterLoad: TNotifyEvent;
    FOnBeforeSave: TNotifyEvent;
    procedure Recount(Index: integer);
    function _GetString(ParaIndex: integer): string;
    procedure _PutString(ParaIndex: integer; const S: string);
    procedure ReformatParagraph(ParaIndex: integer);
    procedure Reformat;
    procedure CheckLength(const st: string);
    procedure Grow;
    procedure InsertItem(Index: integer; const S: string);
  protected
    procedure Changed; virtual;
    procedure Changing; virtual;
    function Get(Index: integer): string; override;
    function GetParaString(Index: integer): string;
    function GetParagraph(Index: integer): PParagraph;
    procedure Put(Index: integer; const S: string); override;
    procedure PutParaString(Index: integer; const S: string);
    procedure PutObject(Index: Integer; AObject: TObject); override;
    function GetObject(Index: Integer): TObject; override;
    procedure SetUpdateState(Updating: Boolean); override;
    function GetTextStr: string; override;
    procedure SetTextStr(const Value: string); override;
    procedure SetInternal(Index: integer; const Value: string);
    procedure SetInternalParaStr(Index: integer; const Value: string);
    function GetCount: Integer; override; // make compiler happy
    function AddParaStr(ParaIndex: integer; const S: string): integer;
    procedure ReLine; // complete line with spaces until caret X pos
    property Internal[Index: integer]: string write SetInternal;
    property InternalParaStrings[Index: integer]: string write SetInternalParaStr;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear; override;
    procedure BeginUpdate;
    procedure EndUpdate;
    function Add(const S: string): integer; override;
    procedure Delete(Index: integer); override;
    procedure Insert(Index: integer; const S: string); override;
    procedure LoadFromFile(const FileName: string); override;
    procedure SaveToFile(const FileName: string); override;
    procedure SetLockText(const Text: string);
    function GetTextLength: integer; // fast get length(Text) value
    function HasText: boolean; // true if Text<>''
    procedure Index2ParaIndex(Index: integer; out Para, ParaIndex: integer);
    function GetParagraphByIndex(Index: integer; out ParaIndex, IndexOffs: integer): string;
    procedure Caret2Paragraph(X, Y: integer; out ParaIndex, IndexOffs: integer);
    procedure Paragraph2Caret(ParaIndex, IndexOffs: integer; out X, Y: integer);
    function GetParaOffs(ParaIndex: integer): integer; // in global Text[] string
    property ParaLineCount: integer read FParaLinesCount;
    property ParaStrings[Index: integer {=ParaY}]: string read GetParaString write PutParaString;
    property Paragraphs[Index: integer]: PParagraph read GetParagraph; // = FList[index]
    // property OnChange: TNotifyEvent read FOnChange write FOnChange;
    property OnChanging: TNotifyEvent read FOnChanging write FOnChanging;
  end;

  TModifiedAction = (maInsert, maDelete);

  TBookMark = record
    X, Y: integer;
    Valid: boolean;
  end;
  TBookMarkNum = 0..9;
  TBookMarks = array[TBookMarkNum] of TBookMark;

  TEditorClient = class
  private
    FMemoEx: TCustomMemoEx;
    Top: integer;
    function Left: integer;
    function Height: integer;
    function Width: integer;
    function ClientWidth: integer;
    function ClientHeight: integer;
    function ClientRect: TRect;
    function BoundsRect: TRect;
    function GetCanvas: TCanvas;
    property Canvas: TCanvas read GetCanvas;
  end;

  TGutter = class
  private
    FMemoEx: TCustomMemoEx;
  public
    procedure Paint;
    procedure Invalidate;
  end;

  TOnPaintGutter = procedure(Sender: TObject; Canvas: TCanvas; const Rect: TRect) of object;

  TEditCommand = word;

  TMacro = string; { uses as buffer }

  TEditKey = class
  public
    Key1, Key2: Word;
    Shift1, Shift2: TShiftState;
    Command: TEditCommand;
    constructor Create(const ACommand: TEditCommand; const AKey1: word;
      const AShift1: TShiftState);
    constructor Create2(const ACommand: TEditCommand; const AKey1: word;
      const AShift1: TShiftState; const AKey2: word; const AShift2: TShiftState);
  end;

  TKeyboard = class
  private
    List: TList;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState);
    procedure Add2(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState;
      const AKey2: word; const AShift2: TShiftState);
    procedure Clear;
    function Command(const AKey: word; const AShift: TShiftState): TEditCommand;
    function Command2(const AKey1: word; const AShift1: TShiftState;
      const AKey2: word; const AShift2: TShiftState): TEditCommand;
    {$IFDEF MEMOEX_DEFLAYOUT}
    procedure SetDefLayout;
    {$ENDIF MEMOEX_DEFLAYOUT}
  end;

  EComplete = class(EAbort);
  EMemoExError = class(Exception);

  {$IFDEF MEMOEX_UNDO}
  TUndoBuffer = class;

  TUndo = class
  private
    FMemoEx: TCustomMemoEx;
    function UndoBuffer: TUndoBuffer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx);
    procedure Undo; dynamic; abstract;
    procedure Redo; dynamic; abstract;
  end;

  TUndoBuffer = class(TList)
  private
    FMemoEx: TCustomMemoEx;
    FPtr: integer;
    FCancelUndo, InUndo: boolean;
    function IsNewGroup(const AUndo: TUndo): boolean;
  public
    constructor Create;
    procedure Add(AUndo: TUndo);
    function LastUndo: TUndo;
    procedure Undo;
    procedure Redo;
    procedure Clear; override;
    procedure Delete;
  end;
  {$ENDIF MEMOEX_UNDO}

  {$IFDEF MEMOEX_COMPLETION}

  TCompletion = class;

  TOnCompletion = procedure(Sender: TObject; var Cancel: boolean) of object;
  {$ENDIF MEMOEX_COMPLETION}

  TTabStop = (tsTabStop, tsAutoIndent);


  {*** TCustomMemoEx }

  TCustomMemoEx = class(TCustomControl)
  private
    { internal objects }
    FLines: TEditorStrings;
    scbHorz: TRAControlScrollBar95;
    scbVert: TRAControlScrollBar95;
    EditorClient: TEditorClient;
    FGutter: TGutter;
    FKeyboard: TKeyboard;
    FBookMarks: TBookMarks;
    FUpdateLock: integer;
    {$IFDEF MemoEx_UNDO}
    FUndoBuffer: TUndoBuffer;
    FGroupUndo: boolean;
    {$ENDIF MEMOEX_UNDO}
    {$IFDEF MEMOEX_COMPLETION}
    FCompletion: TCompletion;
    {$ENDIF MEMOEX_COMPLETION}
    { internal - Columns and rows attributes }
    FCols, FRows: integer;
    FLeftCol, FTopRow: integer;
    // FLeftColMax, FTopRowMax : integer;
    FLastVisibleCol, FLastVisibleRow: integer;
    FCaretX, FCaretY: integer;
    FVisibleColCount: integer;
    FVisibleRowCount: integer;
    { internal - other flags and attributes }
    FAllRepaint: boolean;
    FCellRect: TCellRect;
    {$IFDEF MEMOEX_EDITOR}
    IgnoreKeyPress: boolean;
    {$ENDIF MEMOEX_EDITOR}
    WaitSecondKey: Boolean;
    Key1: Word;
    Shift1: TShiftState;
    { internal - selection attributes }
    FSelected: boolean;
    FSelBlock: boolean;
    FSelBegX, FSelBegY, FSelEndX, FSelEndY: integer;
    FUpdateSelBegX, FUpdateSelEndX, FUpdateSelBegY, FUpdateSelEndY: integer;
    FSelStartX, FSelStartY: integer;
    FclSelectBC, FclSelectFC: TColor;
    { mouse support }
    timerScroll: TTimer;
    MouseMoveY, MouseMoveXX, MouseMoveYY: integer;
    { internal }
    FTabPos: array of boolean;
    FTabStops: string;
    { internal - primary for TIReader support }
    FEditBuffer: string;
    FPEditBuffer: PChar;
    FEditBufferSize: integer;
    FCompound: integer;
    { FMacro - buffer of TEditCommand, each command represents by two chars }
    FMacro: TMacro;
    FDefMacro: TMacro;
    { visual attributes - properties }
    FBorderStyle: TBorderStyle;
    FGutterColor: TColor;
    FGutterWidth: integer;
    FRightMarginVisible: boolean;
    FRightMargin: integer;
    FRightMarginColor: TColor;
    FScrollBars: TScrollStyle;
    FDoubleClickLine: boolean;
    FSmartTab: Boolean;
    FBackSpaceUnindents: Boolean;
    FAutoIndent: Boolean;
    FKeepTrailingBlanks: Boolean;
    FCursorBeyondEOF: Boolean;
    FCursorBeyondEOL: Boolean;
    { FInclusive - Inclusive mode }
    FInclusive: Boolean;
    { non-visual attributes - properties }
    FInsertMode: boolean;
    FReadOnly: boolean;
    FModified: boolean;
    FRecording: boolean;
    { Events }
    FOnGetLineAttr: TOnGetLineAttr;
    FOnChange: TNotifyEvent;
    FOnSelectionChange: TNotifyEvent;
    FOnChangeStatus: TOnChangeStatus;
    FOnChangeClipboardState: TOnChangeClipboardState;
    FOnScroll: TNotifyEvent;
    FOnResize: TNotifyEvent;
    FOnDblClick: TNotifyEvent;
    FOnPaintGutter: TOnPaintGutter;
    FOnWordClick: TOnWordClick;
    FOnMouseOver: TOnMouseOver;
    FOnBreakLine: TOnBreakLine;
    FOnConcatLine: TOnConcatLine;
    FOnTextInsert: TOnTextInsert;
    FOnCaseConversion: TOnCaseConversion;
    FOnInsertBlock: TOnInsertBlock;
    FOnSaveBlock: TOnSaveBlock;
    FOnInsertMacro: TOnInsertMacro;
    FOnBlockOperation: TOnBlockOperation;
    FOnSetCaretPos: TOnSetCaretPos;
    {$IFDEF MEMOEX_COMPLETION}
    FOnCompletionIdentifer: TOnCompletion;
    FOnCompletionTemplate: TOnCompletion;
    FOnCompletionDrawItem: TDrawItemEvent;
    FOnCompletionMeasureItem: TMeasureItemEvent;
    FOnPreprocessCompletion: TOnPreprocessCompletion;
    {$ENDIF MEMOEX_COMPLETION}
    FDrawBitmap: TBitmap;
    FFont: TFont;
    FWantTabs: boolean;
    FWordWrap: boolean;
    FStripInvisible: boolean;
    FParaX, FParaY: integer;
    NextClipViewer: THandle;
    scbVertWidth, scbHorzHeight: integer;
    Max_X: integer;
    mouse_down, mouse_dragged, double_clicked: boolean;
    FWordUnderCursor: TWordUnderCursor;
    FClipPasteRtfBackSlashConvert: boolean;
    {$ifdef CLIPBOARDPROTECT} // ClipProtect will trunc clipboard to 2KB
    FClip: string; // AB
    FClipProtect: boolean;
    {$endif}
    FOnClipboardPaste: TOnClipboardPaste; // AB
    procedure SetMax_X(const Value: integer);
    procedure UpdateEditorSize(const FullUpdate: boolean = true; const RepaintGutter: boolean = true);
    procedure RedrawFrom(YFrom: integer);
    function RepaintParagraph(LineIndex: integer): integer;
    {$IFDEF MEMOEX_COMPLETION}
    procedure DoCompletionIdentifer(var Cancel: boolean);
    procedure DoCompletionTemplate(var Cancel: boolean);
    function DoPreprocessCompletion(const ID, OldText: string): string;
    {$ENDIF MEMOEX_COMPLETION}
    procedure ScrollTimer(Sender: TObject);
    procedure ReLine;
    function GetDefTabStop(const X: integer; const Next: Boolean): integer;
    function GetTabStop(const X, Y: integer; const What: TTabStop; const Next: Boolean): integer;
    function GetBackStop(const X, Y: integer): integer;
    procedure TextAllChangedInternal(const Unselect: Boolean);
    { properties }
    procedure SetGutterWidth(AWidth: integer);
    procedure SetGutterColor(AColor: TColor);
    procedure SetFont(Value: TFont);
    procedure SetBorderStyle(Value: TBorderStyle);
    procedure SetLines(ALines: TEditorStrings);
    function GetRealOffs(DefOffs, Index: integer): integer;
    function GetSelStart: integer;
    procedure SetSelStart(const ASelStart: integer);
    procedure SetSelLength(const ASelLength: integer);
    function GetSelLength: integer;
    procedure SetMode(index: integer; Value: boolean);
    procedure SetCaretPosition(const index, Pos: integer);
    procedure SetCols(ACols: integer);
    procedure SetRows(ARows: integer);
    procedure SetScrollBars(Value: TScrollStyle);
    procedure SetRightMarginVisible(Value: boolean);
    procedure SetRightMargin(Value: integer);
    procedure SetRightMarginColor(Value: TColor);
    function ExtractStringWithStyle(XX, YY: integer; const From: string; Style: word;
      const LineAttrs: TLineAttrs; out start: integer): string;
    procedure GetWordUnderCursor(X, Y: integer; aShift: TShiftState = []);
    function GetAfterLoad: TNotifyEvent;
    procedure SetAfterLoad(Value: TNotifyEvent);
    function GetBeforeSave: TNotifyEvent;
    procedure SetBeforeSave(Value: TNotifyEvent);
    procedure SetWordWrap(Value: boolean);
    procedure SetStripInvisible(Value: boolean);
    procedure SetSelectedText(Value: boolean);
    procedure FontChanged(Sender: TObject);
    procedure SetTopRow(const Value: integer);
    function GetTextStr: string;
    procedure SetTextStr(const Value: string);
  protected
    SelAttrs_Size: integer;
    SelAttrs: TSelAttrs;
    property FSelectedText: boolean read FSelected write SetSelectedText;
    procedure Resize; override;
    procedure CreateWnd; override;
    procedure CreateParams(var Params: TCreateParams); override;
    procedure Loaded; override;
    procedure Paint; override;
    procedure ScrollBarScroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: integer);
    procedure Scroll(const Vert: boolean; const ScrollPos: integer);
    procedure PaintLine(const Line: integer; ColBeg, ColEnd: integer);
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    {$IFDEF MEMOEX_EDITOR}
    procedure KeyPress(var Key: Char); override;
    procedure InsertChar(const Key: Char);
    {$ENDIF MEMOEX_EDITOR}
    procedure SetSel(const ASelX, ASelY: integer);
    function GetAttrDelta(StartFrom, EndTo: integer; const LineAttrs: TLineAttrs): integer;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
    procedure DblClick; override;
    function DoMouseWheel(Shift: TShiftState; WheelDelta: integer; MousePos: TPoint): boolean; override;
    procedure DoContextPopup(MousePos: TPoint; var Handled: Boolean); override;
    // procedure DrawRightMargin;
    procedure PaintSelection;
    procedure SetUnSelected;
    procedure Mouse2Cell(const X, Y: integer; var CX, CY: integer);
    procedure Mouse2Caret(const X, Y: integer; var CX, CY: integer);
    procedure CaretCoord(const X, Y: integer; var CX, CY: integer);
    function PosFromMouse(const X, Y: integer): integer;
    procedure SetLockText(const Text: string);
    // function ExpandTabs(const S: string): string;
    {$IFDEF MEMOEX_UNDO}
    procedure CantUndo;
    {$ENDIF MEMOEX_UNDO}
    procedure SetCaretInternal(X, Y: integer);
    procedure ValidateEditBuffer;
    procedure SetXY(X, Y: integer);
    {$IFDEF MEMOEX_EDITOR}
    procedure ChangeBookMark(const BookMark: TBookMarkNum; const Valid: boolean);
    procedure InsertText(const Text: string);
    {$ENDIF MEMOEX_EDITOR}
    procedure BeginRecord;
    procedure EndRecord(var AMacro: TMacro);
    procedure PlayMacro(const AMacro: TMacro);
    function YinBounds(AY: integer): boolean;
    function DoChangeCase(const st: string; Conversion: byte): string;
    { triggers for descendants }
    procedure Changed; dynamic;
    procedure TextAllChanged; dynamic;
    procedure StatusChanged; dynamic;
    procedure SelectionChanged; dynamic;
    procedure ClipboardChanged; dynamic;
    procedure GetLineAttr(Line, LineIdx, LineOffs, LineLen, ColBeg, ColEnd: integer;
      const ALine: string; var FAttrs: TLineAttrs); virtual;
    procedure GutterPaint(Canvas: TCanvas; const Rect: TRect); dynamic;
    procedure BookmarkChanged(BookMark: integer); dynamic;
    {$IFDEF MEMOEX_COMPLETION}
    procedure CompletionIdentifer(var Cancel: boolean); dynamic;
    procedure CompletionTemplate(var Cancel: boolean); dynamic;
    {$ENDIF MEMOEX_COMPLETION}
    property Gutter: TGutter read FGutter;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Invalidate; override;
    procedure WndProc(var Message: TMessage); override;
    procedure SetLeftTop(ALeftCol, ATopRow: integer);
    procedure ClipBoardCopy;
    procedure ClipBoardPaste;
    procedure ClipBoardCut;
    procedure DeleteSelected;
    function CalcCellRect(const X, Y: integer): TRect;
    procedure SetCaret(const X, Y: integer);
    procedure SetCaretAtParaPos(const ParaIndex, IndexOffs: integer);
    procedure CaretFromPos(Pos: integer; var X, Y: integer);
    function PosFromCaret(X, Y: integer): integer;
    procedure PaintCaret(bShow: boolean);
    function GetTextLen: integer;
    function GetSelText: string;
    procedure SetSelText(const AValue: string);
    function GetWordOnCaret: string;
    procedure BeginUpdate;
    procedure EndUpdate;
    procedure MakeRowVisible(ARow: integer);
    procedure Command(ACommand: TEditCommand); virtual;
    procedure PostCommand(ACommand: TEditCommand);
    {$IFDEF MEMOEX_EDITOR}
    procedure InsertTextAtCurrentPos(const _Text: string);
    function FindNext(const text: string; ignCase: boolean): boolean;
    procedure ReplaceWord(const NewString: string);
    procedure ReplaceWord2(const NewString: string);
    {$ENDIF}
    procedure BeginCompound;
    procedure EndCompound;
    function GetText(Position: longint; Buffer: PChar; Count: longint): longint;
    function IsUndoEmpty: boolean;
    procedure MouseWheelScroll(Delta: integer);
    {$ifdef CLIPBOARDPROTECT} // ClipProtect will trunc clipboard to 2KB
    property ClipProtect: boolean read FClipProtect write FClipProtect; // AB
    {$endif}
    property ClipPasteRtfBackSlashConvert: boolean // AB: rtf
      read FClipPasteRtfBackSlashConvert write FClipPasteRtfBackSlashConvert;
    property LeftCol: integer read FLeftCol;
    property TopRow: integer read FTopRow write SetTopRow;
    property VisibleColCount: integer read FVisibleColCount;
    property VisibleRowCount: integer read FVisibleRowCount;
    property LastVisibleCol: integer read FLastVisibleCol;
    property LastVisibleRow: integer read FLastVisibleRow;
    property Cols: integer read FCols write SetCols;
    property Rows: integer read FRows write SetRows;
    property CaretX: integer index 0 read FCaretX write SetCaretPosition;
    property CaretY: integer index 1 read FCaretY write SetCaretPosition;
    property Modified: boolean read FModified write FModified;
    property SelStart: integer read GetSelStart write SetSelStart;
    property SelLength: integer read GetSelLength write SetSelLength;
    property SelText: string read GetSelText write SetSelText;
    property SelectedText: boolean read FSelected;
    property BookMarks: TBookMarks read FBookMarks;
    property Keyboard: TKeyboard read FKeyboard;
    property CellRect: TCellRect read FCellRect;
    {$IFDEF MEMOEX_UNDO}
    property UndoBuffer: TUndoBuffer read FUndoBuffer;
    {$ENDIF MEMOEX_UNDO}
    property Recording: boolean read FRecording;
    property WordUnderCursor: TWordUnderCursor read FWordUnderCursor;
    property SelBegY: integer read FSelBegY;
    property SelEndY: integer read FSelEndY;
  public
    { published in descendants }
    property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle default bsSingle;
    property Lines: TEditorStrings read FLines write SetLines;
    property ScrollBars: TScrollStyle read FScrollBars write SetScrollBars default ssBoth;
    property Cursor default crIBeam;
    property Color default clWindow;
    property Font: TFont read FFont write SetFont;
    property Text: string read GetTextStr write SetTextStr;
    property GutterWidth: integer read FGutterWidth write SetGutterWidth;
    property GutterColor: TColor read FGutterColor write SetGutterColor default clBtnFace;
    property RightMarginVisible: boolean read FRightMarginVisible write SetRightMarginVisible default true;
    property RightMargin: integer read FRightMargin write SetRightMargin default 80;
    property RightMarginColor: TColor read FRightMarginColor write SetRightMarginColor default clBtnFace;
    property InsertMode: boolean index 0 read FInsertMode write SetMode default true;
    property ReadOnly: boolean index 1 read FReadOnly write SetMode default false;
    property DoubleClickLine: boolean read FDoubleClickLine write FDoubleClickLine default false;
    {$IFDEF MEMOEX_COMPLETION}
    property Completion: TCompletion read FCompletion write FCompletion;
    {$ENDIF MEMOEX_COMPLETION}
    property TabStops: string read FTabStops write FTabStops;
    property SmartTab: Boolean read FSmartTab write FSmartTab default true;
    property BackSpaceUnindents: Boolean read FBackSpaceUnindents write FBackSpaceUnindents default true;
    property AutoIndent: Boolean read FAutoIndent write FAutoIndent default true;
    property KeepTrailingBlanks: Boolean read FKeepTrailingBlanks write FKeepTrailingBlanks default false;
    property CursorBeyondEOF: Boolean read FCursorBeyondEOF write FCursorBeyondEOF default false;
    property CursorBeyondEOL: Boolean read FCursorBeyondEOL write FCursorBeyondEOL default true;
    property SelForeColor: TColor read FclSelectFC write FclSelectFC;
    property SelBackColor: TColor read FclSelectBC write FclSelectBC;
    property StripInvisible: boolean read FStripInvisible write SetStripInvisible default false;
    property WantTabs: boolean read FWantTabs write FWantTabs default true;
    property WordWrap: boolean read FWordWrap write SetWordWrap default true;
    property OnAfterLoad: TNotifyEvent read GetAfterLoad write SetAfterLoad;
    property OnBeforeSave: TNotifyEvent read GetBeforeSave write SetBeforeSave;
    property OnGetLineAttr: TOnGetLineAttr read FOnGetLineAttr write FOnGetLineAttr;
    property OnChangeStatus: TOnChangeStatus read FOnChangeStatus write FOnChangeStatus;
    property OnChangeClipboardState: TOnChangeClipboardState read FOnChangeClipboardState write FOnChangeClipboardState;
    property OnScroll: TNotifyEvent read FOnScroll write FOnScroll;
    property OnResize: TNotifyEvent read FOnResize write FOnResize;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    property OnSelectionChange: TNotifyEvent read FOnSelectionChange write FOnSelectionChange;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnDblClick: TNotifyEvent read FOnDblClick write FOnDblClick;
    property OnPaintGutter: TOnPaintGutter read FOnPaintGutter write FOnPaintGutter;
    property OnMouseOver: TOnMouseOver read FOnMouseOver write FOnMouseOver;
    property OnWordClick: TOnWordClick read FOnWordClick write FOnWordClick;
    property OnBreakLine: TOnBreakLine read FOnBreakLine write FOnBreakLine;
    property OnConcatLine: TOnConcatLine read FOnConcatLine write FOnConcatLine;
    property OnTextInsert: TOnTextInsert read FOnTextInsert write FOnTextInsert;
    property OnCaseConversion: TOnCaseConversion read FOnCaseConversion write FOnCaseConversion;
    property OnInsertBlock: TOnInsertBlock read FOnInsertBlock write FOnInsertBlock;
    property OnSaveBlock: TOnSaveBlock read FOnSaveBlock write FOnSaveBlock;
    property OnInsertMacro: TOnInsertMacro read FOnInsertMacro write FOnInsertMacro;
    property OnBlockOperation: TOnBlockOperation read FOnBlockOperation write FOnBlockOperation;
    property OnSetCaretPos: TOnSetCaretPos read FOnSetCaretPos write FOnSetCaretPos;
    property OnClipboardPaste: TOnClipboardPaste read FOnClipboardPaste write FOnClipboardPaste;
    {$IFDEF MEMOEX_COMPLETION}
    property OnCompletionIdentifer: TOnCompletion read FOnCompletionIdentifer write FOnCompletionIdentifer;
    property OnCompletionTemplate: TOnCompletion read FOnCompletionTemplate write FOnCompletionTemplate;
    property OnCompletionDrawItem: TDrawItemEvent read FOnCompletionDrawItem write FOnCompletionDrawItem;
    property OnCompletionMeasureItem: TMeasureItemEvent read FOnCompletionMeasureItem write FOnCompletionMeasureItem;
    property OnPreprocessCompletion: TOnPreprocessCompletion read FOnPreprocessCompletion write FOnPreprocessCompletion;
    {$ENDIF MEMOEX_COMPLETION}
    property DockManager;
  end;

  TMemoEx = class(TCustomMemoEx)
  public
    /// a JSON syntax highlighter
    class procedure JSONLineAttr(Sender: TObject; const Line: string;
      index: Integer; const SelAttrs: TSelAttrs; var Attrs: TLineAttrs);
  published
    property TabOrder;
    property BorderStyle;
    property Lines;
    property ScrollBars;
    property GutterWidth;
    property GutterColor;
    property RightMarginVisible;
    property RightMargin;
    property RightMarginColor;
    property InsertMode;
    property ReadOnly;
    property DoubleClickLine;
    {$IFDEF MEMOEX_COMPLETION}
    property Completion;
    {$ENDIF MEMOEX_COMPLETION}
    property TabStops;
    property SmartTab;
    property BackSpaceUnindents;
    property AutoIndent;
    property KeepTrailingBlanks;
    property CursorBeyondEOF;
    property CursorBeyondEOL;
    property SelForeColor;
    property SelBackColor;
    property Text;
    property StripInvisible;
    property OnAfterLoad;
    property OnBeforeSave;
    property OnEnter;
    property OnExit;
    property OnGetLineAttr;
    property OnChangeStatus;
    property OnChangeClipboardState;
    property OnScroll;
    property OnResize;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnChange;
    property OnSelectionChange;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnDblClick;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnPaintGutter;
    property OnMouseOver;
    property OnWordClick;
    property OnBreakLine;
    property OnConcatLine;
    property OnTextInsert;
    property OnCaseConversion;
    property OnInsertBlock;
    property OnSaveBlock;
    property OnInsertMacro;
    property OnBlockOperation;
    property OnSetCaretPos;
    {$IFDEF MEMOEX_COMPLETION}
    property OnCompletionIdentifer;
    property OnCompletionTemplate;
    property OnCompletionDrawItem;
    property OnCompletionMeasureItem;
    property OnPreprocessCompletion;
    {$ENDIF MEMOEX_COMPLETION}
    { TCustomControl }
    property Align;
    property Enabled;
    property Color;
    {$ifndef FPC}
    property Ctl3D;
    property OnCanResize;
    {$endif FPC}
    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 default true;
    property DockSite;
    property DragKind;
    property ParentBiDiMode;
    property WantTabs default true;
    property WordWrap default true;
    property OnConstrainedResize;
    property OnDockDrop;
    property OnDockOver;
    property OnEndDock;
    property OnGetSiteInfo;
    property OnStartDock;
    property OnUnDock;
  end;

  {$IFDEF MEMOEX_COMPLETION}
  TCompletionList = (cmIdentifers, cmTemplates);

  TCompletion = class(TPersistent)
  private
    FMemoEx: TCustomMemoEx;
    FPopupList: TListBox;
    FAutoChange: TStrings;
    FAutoChangeList: TList;
    FIdentifers: TStrings;
    FTemplates: TStrings;
    FItems: TStringList;
    FItemIndex: integer;
    FMode: TCompletionList;
    FDefMode: TCompletionList;
    FItemHeight: integer;
    FTimer: TTimer;
    FEnabled: boolean;
    FVisible: boolean;
    FDropDownCount: integer;
    FDropDownWidth: integer;
    FListBoxStyle: TListBoxStyle;
    FCaretChar: char;
    FCRLF: string;
    FSeparator: string;
    function DoKeyDown(Key: Word; Shift: TShiftState): boolean;
    procedure DoKeyPress(Key: Char);
    procedure OnTimer(Sender: TObject);
    procedure FindSelItem(var Eq: boolean);
    procedure ReplaceWord(const ANewString: string);
    function Cmp1(const S1, S2: string): integer;
    function Cmp2(const S1, S2: string): boolean;
    procedure AutoChangeChanged(Sender: TObject);
    procedure ClearAutoChangeList;
    procedure UpdateAutoChange;
    procedure SetStrings(index: integer; AValue: TStrings);
    function GetItemIndex: integer;
    procedure SetItemIndex(AValue: integer);
    function GetInterval: cardinal;
    procedure SetInterval(AValue: cardinal);
    procedure MakeItems;
    function GetItems: TStrings;
  public
    constructor Create2(AMemoEx: TCustomMemoEx);
    destructor Destroy; override;
    procedure DropDown(const AMode: TCompletionList; const ShowAlways: boolean);
    procedure DoCompletion(const AMode: TCompletionList);
    procedure CloseUp(const Apply: boolean);
    procedure SelectItem;
    property ItemIndex: integer read GetItemIndex write SetItemIndex;
    property Visible: boolean read FVisible write FVisible;
    property Mode: TCompletionList read FMode write FMode;
    property Items: TStringList read FItems;
  published
    property DropDownCount: integer read FDropDownCount write FDropDownCount default 6;
    property DropDownWidth: integer read FDropDownWidth write FDropDownWidth default 300;
    property Enabled: boolean read FEnabled write FEnabled default false;
    property Separator: string read FSeparator write FSeparator;
    property Identifers: TStrings index 0 read FIdentifers write SetStrings;
    property Templates: TStrings index 1 read FTemplates write SetStrings;
    property AutoChange: TStrings index 2 read FAutoChange write SetStrings;
    property ItemHeight: integer read FItemHeight write FItemHeight;
    property Interval: cardinal read GetInterval write SetInterval;
    property ListBoxStyle: TListBoxStyle read FListBoxStyle write FListBoxStyle;
    property CaretChar: char read FCaretChar write FCaretChar;
    property CRLF: string read FCRLF write FCRLF;
  end;
  {$ENDIF MEMOEX_COMPLETION}

const

 { Editor commands }

  ecCharFirst = $00;
  ecCharLast = $FF;
  ecCommandFirst = $100;
  ecUser = $8000; { use this for descendants }
  {Cursor}
  ecLeft = ecCommandFirst + 1;
  ecUp = ecLeft + 1;
  ecRight = ecLeft + 2;
  ecDown = ecLeft + 3;
  {Cursor with select}
  ecSelLeft = ecCommandFirst + 9;
  ecSelUp = ecSelLeft + 1;
  ecSelRight = ecSelLeft + 2;
  ecSelDown = ecSelLeft + 3;
  {Cursor position change according to word}
  ecPrevWord = ecSelDown + 1;
  ecNextWord = ecPrevWord + 1;
  ecSelPrevWord = ecPrevWord + 2;
  ecSelNextWord = ecPrevWord + 3;
  ecSelWord = ecPrevWord + 4;
  ecWindowTop = ecSelWord + 1;
  ecWindowBottom = ecWindowTop + 1;
  ecPrevPage = ecWindowTop + 2;
  ecNextPage = ecWindowTop + 3;
  ecSelPrevPage = ecWindowTop + 4;
  ecSelNextPage = ecWindowTop + 5;
  ecBeginLine = ecSelNextPage + 1;
  ecEndLine = ecBeginLine + 1;
  ecBeginDoc = ecBeginLine + 2;
  ecEndDoc = ecBeginLine + 3;
  ecSelBeginLine = ecBeginLine + 4;
  ecSelEndLine = ecBeginLine + 5;
  ecSelBeginDoc = ecBeginLine + 6;
  ecSelEndDoc = ecBeginLine + 7;
  ecSelAll = ecBeginLine + 8;
  ecScrollLineUp = ecSelAll + 1;
  ecScrollLineDown = ecScrollLineUp + 1;
  ecInsertPara = ecCommandFirst + 101;
  ecBackspace = ecInsertPara + 1;
  ecDelete = ecInsertPara + 2;
  ecChangeInsertMode = ecInsertPara + 3;
  ecTab = ecInsertPara + 4;
  ecBackTab = ecInsertPara + 5;
  ecIndent = ecInsertPara + 6;
  ecUnindent = ecInsertPara + 7;
  ecDeleteSelected = ecInsertPara + 10;
  ecClipboardCopy = ecInsertPara + 11;
  ecClipboardCut = ecClipboardCopy + 1;
  ecClipBoardPaste = ecClipboardCopy + 2;
  ecDeleteLine = ecClipBoardPaste + 1;
  ecDeleteWord = ecDeleteLine + 1;
  ecToUpperCase = ecDeleteLine + 2;
  ecToLowerCase = ecToUpperCase + 1;
  ecChangeCase = ecToUpperCase + 2;
  ecUndo = ecChangeCase + 1;
  ecRedo = ecUndo + 1;
  ecBeginCompound = ecUndo + 2; { not implemented }
  ecEndCompound = ecUndo + 3; { not implemented }
  ecBeginUpdate = ecUndo + 4;
  ecEndUpdate = ecUndo + 5;
  ecSetBookmark0 = ecEndUpdate + 1;
  ecSetBookmark1 = ecSetBookmark0 + 1;
  ecSetBookmark2 = ecSetBookmark0 + 2;
  ecSetBookmark3 = ecSetBookmark0 + 3;
  ecSetBookmark4 = ecSetBookmark0 + 4;
  ecSetBookmark5 = ecSetBookmark0 + 5;
  ecSetBookmark6 = ecSetBookmark0 + 6;
  ecSetBookmark7 = ecSetBookmark0 + 7;
  ecSetBookmark8 = ecSetBookmark0 + 8;
  ecSetBookmark9 = ecSetBookmark0 + 9;
  ecGotoBookmark0 = ecSetBookmark9 + 1;
  ecGotoBookmark1 = ecGotoBookmark0 + 1;
  ecGotoBookmark2 = ecGotoBookmark0 + 2;
  ecGotoBookmark3 = ecGotoBookmark0 + 3;
  ecGotoBookmark4 = ecGotoBookmark0 + 4;
  ecGotoBookmark5 = ecGotoBookmark0 + 5;
  ecGotoBookmark6 = ecGotoBookmark0 + 6;
  ecGotoBookmark7 = ecGotoBookmark0 + 7;
  ecGotoBookmark8 = ecGotoBookmark0 + 8;
  ecGotoBookmark9 = ecGotoBookmark0 + 9;
  ecCompletionIdentifers = ecGotoBookmark9 + 1;
  ecCompletionTemplates = ecCompletionIdentifers + 1;
  ecRecordMacro = ecCompletionTemplates + 1;
  ecPlayMacro = ecRecordMacro + 1;
  ecBeginRecord = ecRecordMacro + 2;
  ecEndRecord = ecRecordMacro + 3;
  ecSaveBlock = ecEndRecord + 1;
  ecInsertBlock = ecSaveBlock + 1;
  ecInsertMacro0 = ecInsertBlock + 1;
  ecInsertMacro1 = ecInsertMacro0 + 1;
  ecInsertMacro2 = ecInsertMacro0 + 2;
  ecInsertMacro3 = ecInsertMacro0 + 3;
  ecInsertMacro4 = ecInsertMacro0 + 4;
  ecInsertMacro5 = ecInsertMacro0 + 5;
  ecInsertMacro6 = ecInsertMacro0 + 6;
  ecInsertMacro7 = ecInsertMacro0 + 7;
  ecInsertMacro8 = ecInsertMacro0 + 8;
  ecInsertMacro9 = ecInsertMacro0 + 9;
  ecInsertMacroA = ecInsertMacro0 + 10;
  ecInsertMacroB = ecInsertMacro0 + 11;
  ecInsertMacroC = ecInsertMacro0 + 12;
  ecInsertMacroD = ecInsertMacro0 + 13;
  ecInsertMacroE = ecInsertMacro0 + 14;
  ecInsertMacroF = ecInsertMacro0 + 15;
  ecInsertMacroG = ecInsertMacro0 + 16;
  ecInsertMacroH = ecInsertMacro0 + 17;
  ecInsertMacroI = ecInsertMacro0 + 18;
  ecInsertMacroJ = ecInsertMacro0 + 19;
  ecInsertMacroK = ecInsertMacro0 + 20;
  ecInsertMacroL = ecInsertMacro0 + 21;
  ecInsertMacroM = ecInsertMacro0 + 22;
  ecInsertMacroN = ecInsertMacro0 + 23;
  ecInsertMacroO = ecInsertMacro0 + 24;
  ecInsertMacroP = ecInsertMacro0 + 25;
  ecInsertMacroQ = ecInsertMacro0 + 26;
  ecInsertMacroR = ecInsertMacro0 + 27;
  ecInsertMacroS = ecInsertMacro0 + 28;
  ecInsertMacroT = ecInsertMacro0 + 29;
  ecInsertMacroU = ecInsertMacro0 + 30;
  ecInsertMacroV = ecInsertMacro0 + 31;
  ecInsertMacroW = ecInsertMacro0 + 32;
  ecInsertMacroX = ecInsertMacro0 + 33;
  ecInsertMacroY = ecInsertMacro0 + 34;
  ecInsertMacroZ = ecInsertMacro0 + 35;
  ecBlockOpA = ecInsertMacroZ + 1;
  ecBlockOpB = ecBlockOpA + 1;
  ecBlockOpC = ecBlockOpA + 2;
  ecBlockOpD = ecBlockOpA + 3;
  ecBlockOpE = ecBlockOpA + 4;
  ecBlockOpF = ecBlockOpA + 5;
  ecBlockOpG = ecBlockOpA + 6;
  ecBlockOpH = ecBlockOpA + 7;
  ecBlockOpI = ecBlockOpA + 8;
  ecBlockOpJ = ecBlockOpA + 9;
  ecBlockOpK = ecBlockOpA + 10;
  ecBlockOpL = ecBlockOpA + 11;
  ecBlockOpM = ecBlockOpA + 12;
  ecBlockOpN = ecBlockOpA + 13;
  ecBlockOpO = ecBlockOpA + 14;
  ecBlockOpP = ecBlockOpA + 15;
  ecBlockOpQ = ecBlockOpA + 16;
  ecBlockOpR = ecBlockOpA + 17;
  ecBlockOpS = ecBlockOpA + 18;
  ecBlockOpT = ecBlockOpA + 19;
  ecBlockOpU = ecBlockOpA + 20;
  ecBlockOpV = ecBlockOpA + 21;
  ecBlockOpW = ecBlockOpA + 22;
  ecBlockOpX = ecBlockOpA + 23;
  ecBlockOpY = ecBlockOpA + 24;
  ecBlockOpZ = ecBlockOpA + 25;
  ecBackword = ecBlockOpZ + 1;
  ecScrollPageUp = ecBackword + 1;
  ecScrollPageDown = ecScrollPageUp + 1;
  twoKeyCommand = High(word);

const
  __Brackets =['(', ')', '[', ']', '{', '}'];
  __StdWordDelims =[#0..' ', ',', '.', ';', '\', ':', '''', '`']{ + __Brackets};

procedure Register;

function Max(x, y: integer): integer; {$ifdef HASINLINE}inline;{$endif}

function Min(x, y: integer): integer; {$ifdef HASINLINE}inline;{$endif}


implementation

uses
  {$ifndef FPC}
  Consts,
  {$endif FPC}
  RTLConsts;

{$ifndef CPUX86}
function PosEx(const SubStr, S: string; Offset: Integer = 1): Integer; inline;
begin
  Result := System.Pos(SubStr, S, Offset);
end;
{$else}
function PosEx(const SubStr, S: AnsiString; Offset: Cardinal = 1): Integer;
// Faster Equivalent of Delphi 7 StrUtils.PosEx
asm
  push    ebx
  push    esi
  push    edx                 // @Str
  test    eax,eax
  jz      @@NotFound          // Exit if SubStr = ''
  test    edx,edx
  jz      @@NotFound          // Exit if Str = ''
  mov     esi,ecx
  mov     ecx,[edx-4]         // Length(Str)
  mov     ebx,[eax-4]         // Length(Search string)
  add     ecx,edx
  sub     ecx,ebx             // ecx = Max Start Pos for Full Match
  lea     edx,[edx+esi-1]     // edx = Start Position
  cmp     edx,ecx
  jg      @@NotFound          // StartPos > Max Start Pos
  cmp     ebx,1               // Length(SubStr)
  jle     @@SingleChar        // Length(SubStr) <= 1
  push    edi
  push    ebp
  lea     edi,[ebx-2]         // edi = Length(Search string) - 2
  mov     esi,eax             // esi = Search string
  movzx   ebx,byte ptr [eax]  // bl = Search Character
@@Loop:                       // Compare 2 Characters per Loop
  cmp     bl,[edx]
  je      @@Char1Found
@@NotChar1:
  cmp     bl,[edx+1]
  je      @@Char2Found
@@NotChar2:
  lea     edx,[edx+2]
  cmp     edx,ecx             // Next Start Position <= Max Start Position
  jle     @@Loop
  pop     ebp
  pop     edi
@@NotFound:
  xor     eax,eax            // returns 0 if not found
  pop     edx
  pop     esi
  pop     ebx
  ret
@@Char1Found:
  mov     ebp,edi             // ebp = Length(Search string) - 2
@@Char1Loop:
  movzx   eax,word ptr [esi+ebp]
  cmp     ax,[edx+ebp]       // Compare 2 Chars per Char1Loop (may include #0)
  jne     @@NotChar1
  sub     ebp,2
  jnc     @@Char1Loop
  pop     ebp
  pop     edi
  jmp     @@SetResult
@@Char2Found:
  mov     ebp,edi             // ebp = Length(Search string) - 2
@@Char2Loop:
  movzx   eax,word ptr [esi+ebp]
  cmp     ax,[edx+ebp+1]     // Compare 2 Chars per Char2Loop (may include #0)
  jne     @@NotChar2
  sub     ebp,2
  jnc     @@Char2Loop
  pop     ebp
  pop     edi
  jmp     @@CheckResult
@@SingleChar:
  jl      @@NotFound          // Needed for Zero-Length Non-NIL Strings
  movzx   eax,byte ptr [eax]  // Search Character
@@CharLoop:
  cmp     al,[edx]
  je      @@SetResult
  cmp     al,[edx+1]
  je      @@CheckResult
  lea     edx,[edx+2]
  cmp     edx,ecx
  jle     @@CharLoop
  jmp     @@NotFound
@@CheckResult:                // Check within AnsiString
  cmp     edx,ecx
  jge     @@NotFound
  add     edx,1
@@SetResult:
  pop     ecx                 // @Str
  pop     esi
  pop     ebx
  neg     ecx
  lea     eax,[edx+ecx+1]
end;
{$endif UNICODE}

function StringReplaceAll(const S, OldPattern, NewPattern: string): string;
// fast replacement of StringReplace(S, OldPattern, NewPattern,[rfReplaceAll]);

  procedure Process(j: integer);
  var
    i: integer;
  begin
    Result := '';
    i := 1;
    repeat
      Result := Result + Copy(S, i, j - i) + NewPattern;
      i := j + length(OldPattern);
      j := PosEx(OldPattern, S, i);
      if j = 0 then
      begin
        Result := Result + Copy(S, i, maxInt);
        break;
      end;
    until false;
  end;

var
  j: integer;
begin
  j := PosEx(OldPattern, S);
  if j = 0 then
    result := S
  else
    Process(j);
end;

const
  _AutoChangePunctuation: set of AnsiChar =
    [' ', '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
     '_', '-', '+', '=', ';', ':', '''', '"', '[', ']', '{', '}', ',',
     '.', '/', '?', '<', '>'];

function Max(x, y: integer): integer;
begin
  if x > y then
    Result := x
  else
    Result := y;
end;

function Min(x, y: integer): integer;
begin
  if x < y then
    Result := x
  else
    Result := y;
end;

function GetWordOnPosEx(const S: string; const P: integer; out iBeg, iEnd: integer): string;
begin
  Result := '';
  if (P > Length(S)) or (P < 1) then
    exit;
  iBeg := P;
  if {$ifdef UNICODE} (S[P] < #255) and {$endif} (AnsiChar(S[P]) in Separators) then
    if (P < 1) or ({$ifdef UNICODE}(S[P - 1] < #255) and {$endif} (AnsiChar(S[P - 1]) in Separators)) then
      inc(iBeg)
    else if {$ifdef UNICODE} (S[P - 1] > #255) or {$endif} not (AnsiChar(S[P - 1]) in Separators) then
      dec(iBeg);
  while iBeg >= 1 do
    if {$ifdef UNICODE} (S[iBeg] < #255) and {$endif} (AnsiChar(S[iBeg]) in Separators) then
      break
    else
      dec(iBeg);
  inc(iBeg);
  iEnd := P;
  while iEnd <= Length(S) do
    if {$ifdef UNICODE} (S[iEnd] < #255) and {$endif} (AnsiChar(S[iEnd]) in Separators) then
      break
    else
      inc(iEnd);
  if iEnd > iBeg then
    Result := Copy(S, iBeg, iEnd - iBeg)
  else
    Result := S[P];
end;

var
  CF_MEMOEX: integer = 0; // indicates MemoEx clipboard

function _CopyToClipboard(Handle: THandle; const SelText: string; FontCharset: TFontCharset): boolean;
var
  Data: HGLOBAL;
  _Text: PChar;
  fmt: UINT;
begin
  Result := false;
  if SelText <> '' then
  begin
    Data := GlobalAlloc(GMEM_MOVEABLE or GMEM_ZEROINIT, (length(SelText) + 1) * SizeOf(char));
    if Data <> 0 then
    begin
      _Text := GlobalLock(Data);
      if Assigned(_Text) then
      begin
        StrCopy(_Text, PChar(SelText));
        GlobalUnlock(Data);
        {$ifdef UNICODE}
        fmt := CF_UNICODETEXT;
        {$else}
        if FontCharset = OEM_CHARSET then
          fmt := CF_OEMTEXT
        else
          fmt := CF_TEXT;
        {$endif}
        if OpenClipboard(Handle) then
        begin
          EmptyClipboard;
          SetClipboardData(fmt, Data);
          SetClipboardData(CF_MEMOEX, 0); // indicates from MemoEx
          CloseClipboard;
          Result := true;
        end
        else
          GlobalFree(Data);
      end
      else
        GlobalFree(Data);
    end;
  end;
end;

function _PasteFromClipboard(Handle: THandle; FontCharset: TFontCharset;
  {$ifdef CLIPBOARDPROTECT}const InternalClip: string;{$endif}
  DeleteCRLF: boolean = true): string;
var
  fmtText, fmtOEMText: boolean;
  Data: HGLOBAL;
  fmt: UINT;
  _Text: PChar;
  Txt: string;
  i: integer;
begin
{$ifdef CLIPBOARDPROTECT} // ClipProtect will trunc clipboard to 2KB
  if IsClipboardFormatAvailable(CF_MEMOEX) and (InternalClip <> '') then
  begin
    result := InternalClip;
    exit;
  end;
{$endif}
  Result := '';
  {$ifdef UNICODE}
  fmtText := IsClipboardFormatAvailable(CF_UNICODETEXT);
  fmtOEMText := IsClipboardFormatAvailable(CF_TEXT);
  {$else}
  fmtText := IsClipboardFormatAvailable(CF_TEXT);
  fmtOEMText := IsClipboardFormatAvailable(CF_OEMTEXT);
  {$endif}
  if fmtText or fmtOEMText then
    if OpenClipboard(Handle) then
    begin
      {$ifdef UNICODE}
      if not fmtText then
        fmt := CF_TEXT
      else
        fmt := CF_UNICODETEXT;
      {$else}
      if not fmtText then
        fmt := CF_OEMTEXT
      else
        fmt := CF_TEXT;
      {$endif}
      Data := GetClipboardData(fmt);
      if Data <> 0 then
      begin
        _Text := GlobalLock(Data);
        if Assigned(_Text) then
        begin
          {$ifdef UNICODE}
          if fmt = CF_UNICODETEXT then
            Txt := _Text
          else
            Txt := UnicodeString(AnsiString(PAnsiChar(_Text)));
          {$else}
          Txt := _Text;
          if (FontCharset = OEM_CHARSET) and (fmt = CF_TEXT) then
            CharToOEM(PChar(Txt), PChar(Txt))
          else if (FontCharset = DEFAULT_CHARSET) and (fmt = CF_OEMTEXT) then
            OEMToChar(PChar(Txt), PChar(Txt));
          {$endif}
          if DeleteCRLF then
          begin
            i := 1;
            while i <= length(Txt) do
              if Ord(Txt[i]) in [10, 13] then
                System.Delete(Txt, i, 1)
              else
              begin
                if Txt[i] < ' ' then // prevent TAB bug
                  Txt[i] := ' ';
                inc(i);
              end;
          end
          else
            for i := 1 to length(Txt) do
              if ord(Txt[i]) in [1..9, 11, 12, 14..31] then // prevent TAB bug
                Txt[i] := ' ';
          Result := Txt;
          GlobalUnlock(Data);
        end;
      end;
      CloseClipboard;
    end;
end;

function GetWordOnPos(const S: string; const P: integer): string;
var
  i, Beg: integer;
begin
  Result := '';
  if (P > Length(S)) or (P < 1) then
    exit;
  for i := P downto 1 do
    if {$ifdef UNICODE} (S[i] < #255) and {$endif} (AnsiChar(S[i]) in Separators) then
      break;
  Beg := i + 1;
  for i := P to Length(S) do
    if {$ifdef UNICODE} (S[i] < #255) and {$endif} (AnsiChar(S[i]) in Separators) then
      break;
  if i > Beg then
    Result := Copy(S, Beg, i - Beg)
  else
    Result := S[P];
end;

function SubStr(const S: string; const index: integer; const Separator: string): string;
// used on word completion
var
  i: integer;
  pB, pE: PChar;
begin
  Result := '';
  if (index < 0) or ((index = 0) and (Length(S) > 0) and (S[1] = Separator)) then
    exit;
  pB := PChar(S);
  for i := 1 to index do
  begin
    pB := StrPos(pB, PChar(Separator));
    if pB = nil then
      exit;
    pB := pB + Length(Separator);
  end;
  pE := StrPos(pB + 1, PChar(Separator));
  if pE = nil then
    pE := PChar(S) + Length(S);
  if not (StrLIComp(pB, PChar(Separator), Length(Separator)) = 0) then
    SetString(Result, pB, pE - pB);
end;

function KeyPressed(VK: integer): boolean;
begin
  Result := GetKeyState(VK) and $8000 = $8000;
end;


{ TRAControlScrollBar95 }

constructor TRAControlScrollBar95.Create;
begin
  FPage := 1;
  FSmallChange := 1;
  FLargeChange := 1;
end;

const
  SBKIND: array[TScrollBarKind] of integer = (SB_HORZ, SB_VERT);

procedure TRAControlScrollBar95.SetParams(AMin, AMax, APosition, APage: integer);
var
  SCROLLINFO: TSCROLLINFO;
begin
  if AMax < AMin then
    raise EInvalidOperation.Create(SScrollBarRange);
  if APosition < AMin then
    APosition := AMin;
  if APosition > AMax then
    APosition := AMax;
  if Handle > 0 then
  begin
    with SCROLLINFO do
    begin
      cbSize := sizeof(TSCROLLINFO);
      fMask := SIF_DISABLENOSCROLL;
      if (AMin >= 0) or (AMax >= 0) then
        fMask := fMask or SIF_RANGE;
      if APosition >= 0 then
        fMask := fMask or SIF_POS;
      if APage >= 0 then
        fMask := fMask or SIF_PAGE;
      nPos := APosition;
      nMin := AMin;
      nMax := AMax;
      nPage := APage;
    end;
    SetScrollInfo(Handle,  // handle of window with scroll bar
      SBKIND[Kind],        // scroll bar flag
      SCROLLINFO,          // pointer to structure with scroll parameters
      true                 // redraw flag
    );
  end;
end;

procedure TRAControlScrollBar95.SetParam(index, Value: Integer);
begin
  case index of
    0:
      FMin := Value;
    1:
      FMax := Value;
    2:
      FPosition := Value;
    3:
      FPage := Value;
  end;
  if FMax < FMin then
    raise EInvalidOperation.Create(SScrollBarRange);
  if FPosition < FMin then
    FPosition := FMin;
  if FPosition > FMax then
    FPosition := FMax;
  SetParams(FMin, FMax, FPosition, FPage);
end;

procedure TRAControlScrollBar95.DoScroll(var Message: TWMScroll);
var
  ScrollPos: Integer;
  NewPos: Longint;
  ScrollInfo: TScrollInfo;
begin
  with Message do
  begin
    NewPos := FPosition;
    case TScrollCode(ScrollCode) of
      scLineUp:
        Dec(NewPos, FSmallChange);
      scLineDown:
        Inc(NewPos, FSmallChange);
      scPageUp:
        Dec(NewPos, FLargeChange);
      scPageDown:
        Inc(NewPos, FLargeChange);
      scPosition, scTrack:
        with ScrollInfo do
        begin
          cbSize := SizeOf(ScrollInfo);
          fMask := SIF_ALL;
          GetScrollInfo(Handle, SBKIND[Kind], ScrollInfo);
          NewPos := nTrackPos;
        end;
      scTop:
        NewPos := FMin;
      scBottom:
        NewPos := FMax;
    end;
    if NewPos < FMin then
      NewPos := FMin;
    if NewPos > FMax then
      NewPos := FMax;
    ScrollPos := NewPos;
    Scroll(TScrollCode(ScrollCode), ScrollPos);
  end;
  Position := ScrollPos;
end;

procedure TRAControlScrollBar95.Scroll(ScrollCode: TScrollCode; var ScrollPos: Integer);
begin
  if Assigned(FOnScroll) then
    FOnScroll(Self, ScrollCode, ScrollPos);
end;



{$IFDEF MEMOEX_UNDO}

type
  TCaretUndo = class(TUndo)
  private
    FCaretX, FCaretY: integer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer);
    procedure Undo; override;
    procedure Redo; override;
  end;

  TInsertUndo = class(TCaretUndo)
  private
    FText: string;
    FOffset, FParaOffset: integer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
      const AText: string);
    procedure Undo; override;
  end;

  TReLineUndo = class(TInsertUndo);

  TInsertTabUndo = class(TInsertUndo);

  TOverwriteUndo = class(TCaretUndo)
  private
    FOldText, FNewText: string;
    FOffset: integer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
      const AOldText, ANewText: string);
    procedure Undo; override;
  end;

  TDeleteUndo = class(TInsertUndo)
  public
    procedure Undo; override;
  end;

  TDeleteTrailUndo = class(TDeleteUndo);

  TBackspaceUndo = class(TDeleteUndo)
  public
    procedure Undo; override;
  end;

  TReplaceUndo = class(TCaretUndo)
  private
    FBeg, FEnd: integer;
    FText, FNewText: string;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
      const ABeg, AEnd: integer; const AText, ANewText: string);
    procedure Undo; override;
  end;

  TDeleteSelectedUndo = class(TDeleteUndo)
  private
    FSelBlock: boolean; { vertical block }
    FSelBegX, FSelBegY, FSelEndX, FSelEndY, FSelOffs: integer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
      const AText: string; const ASelBlock: boolean; const ASelBegX, ASelBegY,
      ASelEndX, ASelEndY, ASelOffs: integer);
    procedure Undo; override;
  end;

  TSelectUndo = class(TCaretUndo)
  private
    FSelBlock: boolean; { vertical block }
    FSelBegX, FSelBegY, FSelEndX, FSelEndY: integer;
  public
    constructor Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
      const ASelBlock: boolean; const ASelBegX, ASelBegY, ASelEndX, ASelEndY: integer);
    procedure Undo; override;
  end;

  TUnselectUndo = class(TSelectUndo);

  TBeginCompoundUndo = class(TUndo)
  public
    procedure Undo; override;
  end;

  TEndCompoundUndo = class(TBeginCompoundUndo);

{$ENDIF MEMOEX_UNDO}

procedure Err;
begin
  MessageBeep(0);
end;

function FindNotBlankCharPos(const S: string): integer;
begin
  for result := 1 to Length(S) do
    if S[result] <> ' ' then
      Exit;
  Result := 1;
end;

function ANSIChangeCase(const S: string): string;
var
  i: integer;
  Up: char;
begin
  Result := S;
  for i := 1 to Length(Result) do
  begin
    Up := upcase(Result[i]);
    if Result[i] = Up then
      Result[i] := chr(ord(Result[i]) + 32)
    else
      Result[i] := Up;
  end;
end;

type
  {$ifdef CPU64}
  PPtrInt = ^Int64;
  {$else}
  PPtrInt = ^integer;
  {$endif}

function StringDynArrayGetSize(V: PPtrInt; n: integer): integer;
// usage: Siz := StringDynArrayGetSize(pointer(FStrings),FCount);
var
  i: integer;
begin
  result := 0;
  for i := 1 to n do begin
    {$ifdef FPC}
    inc(result, length(string(V^));
    {$else}
    if V^ <> 0 then
      inc(result, PInteger(V^ - 4)^);
    {$endif}
    inc(V);
  end;
end;

function StringDynArrayToPChar(V: PPtrInt; n: integer; P: PChar): PChar;
// usage: StringDynArrayToPChar(pointer(FStrings),FCount,pointer(Result))
var
  i, Size: integer;
begin
  for i := 1 to n do begin
    {$ifdef FPC}
    size := length(V^);
    if size <> 0 then
    begin
    {$else}
    if V^ <> 0 then
    begin
      size := PInteger(V^ - 4)^;
    {$endif}
      move(pointer(V^)^, P^, size * SizeOf(char));
      inc(P, size);
    end;
    inc(V);
  end;
  result := P;
end;


{ TEditorStrings }

procedure TEditorStrings.LoadFromFile(const FileName: string);
begin
  BeginUpdate;
  inherited LoadFromFile(FileName);
  if Assigned(FOnAfterLoad) then
    FOnAfterLoad(FMemoEx);
  EndUpdate;
end;

procedure TEditorStrings.SaveToFile(const FileName: string);
begin
  if Assigned(FOnBeforeSave) then
    FOnBeforeSave(FMemoEx);
  inherited SaveToFile(FileName);
end;

procedure TEditorStrings.Recount(Index: integer);
var
  i: integer;
begin
  if FCount = 0 then
    exit;
  if Index = 0 then
  begin
    FList[0].FPreCount := 0;
    inc(Index);
  end;
  for i := Index to FCount - 1 do
    with FList[i - 1] do
      FList[i].FPreCount := FPreCount + FCount;
end;

procedure TEditorStrings.Index2ParaIndex(Index: integer; out Para, ParaIndex: integer);
var
  L, H, I: integer;
begin
  if (not FMemoEx.FWordWrap) or (FParaLinesCount = FCount) then
  begin
    Para := Index;
    if Para > FCount - 1 then
      Para := FCount - 1;
    ParaIndex := 0;
  end
  else
  begin
    { fast find paragraph index using MinMax (binary search) algo }
    Para := -1;
    ParaIndex := -1;
    L := 0;
    H := FCount - 1;
    while L <= H do
    begin
      I := (L + H) shr 1;
      with FList[I] do
        if Index > FPreCount + FCount - 1 then
          L := I + 1
        else
        begin
          H := I - 1;
          if (Index <= FPreCount + FCount - 1) and (Index >= FPreCount) then
          begin // found
            Para := I;
            ParaIndex := Index - FPreCount;
            break;
          end;
        end;
    end;
  end;
end;

procedure ListIndexError(Index: integer);
// outside procedure -> no temp string -> less heap
{$ifdef CPUX86}
  function ReturnAddr: Pointer;
  asm
        MOV     EAX, [EBP + 4]
  end;
{$endif}
begin
  raise EStringListError.CreateFmt(SListIndexError, [Index]){$ifdef CPUX86} at ReturnAddr{$endif};
end;

function TEditorStrings.GetParagraphByIndex(Index: integer; out ParaIndex, IndexOffs: integer): string;
var
  _P, _PI: integer;
begin
  IndexOffs := 0;
  ParaIndex := 0;
  Result := '';
  Index2ParaIndex(Index, _P, _PI);
  if (_P = -1) or (_PI = -1) then
    ListIndexError(Index);
  ParaIndex := _P;
  result := _GetString(_P);
  IndexOffs := StringDynArrayGetSize(pointer(FList[_P].FStrings), _PI);
end;

procedure TEditorStrings.Caret2Paragraph(X, Y: integer; out ParaIndex, IndexOffs: integer);
var
  _P, _PI: integer;
begin
  ParaIndex := 0;
  IndexOffs := 0;
  Index2ParaIndex(Y, _P, _PI);
  if (_P = -1) or (_PI = -1) then
    ListIndexError(Y);
  ParaIndex := _P;
  IndexOffs := X + StringDynArrayGetSize(pointer(FList[_P].FStrings), _PI);
end;

procedure TEditorStrings.Paragraph2Caret(ParaIndex, IndexOffs: integer; out X, Y: integer);
var
  i, j, k: integer;
  found: boolean;
begin
  X := 0;
  Y := ParaIndex;
  found := false;
  k := 0;
  for i := 0 to FCount - 1 do
    with Flist[i] do
    begin
      if i >= Y then
      begin
        for j := 0 to FCount - 1 do
        begin
          inc(X, length(FStrings[j]));
          if X >= IndexOffs then
          begin
            found := true;
            Y := k + j;
            X := IndexOffs - (X - length(FStrings[j]));
            break;
          end;
        end;
        if found then
          break;
      end;
      inc(k, FCount);
    end;
  if not found then
  begin
    if X > 0 then
    begin
      Y := k;
      X := length(FList[Y].FStrings[FList[Y].FCount - 1]);
      exit;
    end;
    Y := FCount - 1;
    if Y >= 0 then
      X := length(FList[Y].FStrings[FList[Y].FCount - 1])
    else
    begin
      X := 0;
      Y := 0;
    end;
  end;
end;

function TEditorStrings.GetParaOffs(ParaIndex: integer): integer;
var
  i: integer;
begin
  Result := ParaIndex * 2;
  for i := 0 to ParaIndex - 1 do
    with FList[i] do
      inc(Result, StringDynArrayGetSize(pointer(FStrings), FCount));
end;

procedure TEditorStrings.ReformatParagraph(ParaIndex: integer);
var // full rewrite by AB: much faster and use less ram
  s: string;
  c, d, b: PChar;
  L: integer;
begin
  with FList[ParaIndex] do
  begin
    dec(FParaLinesCount, FCount);
    FPreCount := 0;
    if FMemoEx.FWordWrap then
    begin
      s := _GetString(ParaIndex); // whole paragraph text in s
      L := FMemoEx.FRightMargin;
      if length(s) <= L then
      begin
        if FCount > 1 then
        begin
          SetLength(FStrings, 1);
          FCount := 1;
          FStrings[0] := s;
        end;
      end
      else
      begin
        FCount := 0;
        {$ifdef FPC} UniqueString(s); {$endif} // we change FStrings[0] below
        c := @s[1];
        d := c;
        b := c;
        while (c^ <> #0) do
        begin
          if c^ = ' ' then
            b := c; // b = last ' '
          if ((c - d) >= L) then
          begin
            inc(FCount);
            if FCount >= length(FStrings) then
              Setlength(FStrings, FCount + 10);
            if b = d then
              b := d + L; // if no ' '
            SetString(FStrings[FCount - 1], d, b - d + 1);
            c := b + 1;
            b := c;
            d := c;
            if c^ = #0 then
              break;
          end;
          inc(c);
        end;
        if d <> c then
        begin // append last chars (if any) as last line
          inc(FCount);
          SetString(FStrings[FCount - 1], d, c - d);
        end;
        Setlength(FStrings, FCount); // set exact line count
      end;
    end
    else if FCount > 1 then
    begin // something to reformat only if FCount>1
      s := _GetString(ParaIndex);
      SetLength(FStrings, 1);
      FCount := 1;
      FStrings[0] := s;
    end;
    inc(FParaLinesCount, FCount);
  end;
end;

procedure TEditorStrings.Reformat;
var
  i: integer;
begin
  for i := 0 to FCount - 1 do
    ReformatParagraph(i);
  Recount(0);
  Changed;
end;

procedure TEditorStrings.CheckLength(const st: string);
begin
  if length(st) > FMemoEx.Max_X then
    FMemoEx.SetMax_X(length(st) + 1);
end;

constructor TEditorStrings.Create;
begin
  inherited Create;
  FParaLinesCount := 0;
  FOnAfterLoad := nil;
  FOnBeforeSave := nil;
end;

destructor TEditorStrings.Destroy;
begin
  // FOnChange := nil;
  FOnChanging := nil;
  inherited Destroy;
end;

function TEditorStrings.Add(const S: string): integer;
begin
  Result := FCount;
  InsertItem(Result, S);
end;

procedure TEditorStrings.Delete(Index: integer);
begin
  if (Index < 0) or (Index >= FCount) then
    ListIndexError(Index);
  Changing;
  dec(FParaLinesCount, FList[Index].FCount);
  Dec(FCount);
  Finalize(FList[Index]); // avoid memory bug: release deleted FStrings
  if Index < FCount then
  begin
    System.Move(FList[Index + 1], FList[Index], (FCount - Index) * SizeOf(TParagraph));
    FillChar(FList[FCount], sizeof(TParagraph), 0); // avoid memory bug
    Recount(Index);
  end;
  Changed;
end;

procedure TEditorStrings.Insert(Index: integer; const S: string);
begin
  if (Index < 0) or (Index > FCount) then
    ListIndexError(Index);
  InsertItem(Index, S);
end;

procedure TEditorStrings.InsertItem(Index: integer; const S: string);
begin
  Changing;
  if FCount = length(FList) then
    Grow;
  if Index < FCount then
    System.Move(FList[Index], FList[Index + 1], (FCount - Index) * SizeOf(TParagraph));
  fillchar(FList[Index], sizeof(TParagraph), 0); // avoid memory bug
  Inc(FCount);
  AddParaStr(Index, S);
end;

{$WARNINGS OFF}
function TEditorStrings.AddParaStr(ParaIndex: integer; const S: string): integer;
begin
  if (ParaIndex < 0) or (ParaIndex >= FCount) then
    ListIndexError(ParaIndex);
  with FList[ParaIndex] do
  begin
    inc(FCount);
    inc(FParaLinesCount);
    SetLength(FStrings, FCount);
    FStrings[FCount - 1] := S;
    CheckLength(S);
  end;
  ReformatParagraph(ParaIndex);
  Recount(ParaIndex);
  Changed;
end;
{$WARNINGS ON}

procedure TEditorStrings.Changed;
begin
  if (csLoading in FMemoEx.ComponentState) then
    exit;
  if FMemoEx.FUpdateLock = 0 then
  begin
    FMemoEx.TextAllChanged;
    if Assigned(FMemoEx.FOnChange) then
      FMemoEx.FOnChange(Self);
  end;
end;

procedure TEditorStrings.Changing;
begin
  if (FMemoEx.FUpdateLock = 0) and Assigned(FOnChanging) then
    FOnChanging(Self);
end;

procedure TEditorStrings.Clear;
begin
  if FCount <> 0 then
  begin
    Changing;
    FCount := 0;
    FParaLinesCount := 0;
    Finalize(FList); // free all memory
    Changed;
  end;
  fMemoEx.SetCaret(0, 0);
end;

procedure TEditorStrings.BeginUpdate;
begin
  inc(FMemoEx.FUpdateLock);
end;

procedure TEditorStrings.EndUpdate;
begin
  dec(FMemoEx.FUpdateLock);
  Changed;
end;

function TEditorStrings._GetString(ParaIndex: integer): string;
begin
  with FList[ParaIndex] do
    if FCount > 1 then
    begin
      SetString(Result, nil, StringDynArrayGetSize(pointer(FStrings), FCount));
      StringDynArrayToPChar(pointer(FStrings), FCount, pointer(Result));
    end
    else
      result := FStrings[0];
end;

function TEditorStrings.Get(Index: integer): string;
begin
  if cardinal(Index) >= cardinal(FCount) then
    ListIndexError(Index);
  Result := _GetString(Index);
end;

function TEditorStrings.GetParagraph(Index: integer): PParagraph;
begin
  if cardinal(Index) >= cardinal(FCount) then
    ListIndexError(Index);
  Result := @FList[Index];
end;

function TEditorStrings.GetParaString(Index: integer): string;
var
  _P, _PI: integer;
begin
  if not FMemoEx.FWordWrap or (FParaLinesCount = FCount) then
    Result := Get(Index)
  else
  begin
    Index2ParaIndex(Index, _P, _PI);
    if (_P = -1) or (_PI = -1) then
      ListIndexError(Index);
    Result := FList[_P].FStrings[_PI];
  end;
end;

procedure TEditorStrings.Grow;
var
  Delta: integer;
begin
  Delta := length(FList);
  if Delta > 64 * 4 then
    inc(Delta, Delta shr 2)
  else
    inc(Delta, 64); // AB
  SetLength(FList, Delta);
end;

procedure TEditorStrings._PutString(ParaIndex: integer; const S: string);
var
  old_count, old_precount: integer;
begin
  old_count := FList[ParaIndex].FCount;
  old_precount := FList[ParaIndex].FPreCount;
  dec(FParaLinesCount, FList[ParaIndex].FCount);
  with FList[ParaIndex] do
  begin
    FCount := 1;
    SetLength(FStrings, 1);
    FStrings[0] := S;
    FObject := nil;
  end;
  inc(FParaLinesCount);
  ReformatParagraph(ParaIndex);
  if old_count <> FList[ParaIndex].FCount then
    Recount(ParaIndex)
  else
    FList[ParaIndex].FPreCount := old_precount;
end;

procedure TEditorStrings.Put(Index: integer; const S: string);
begin
  if (Index < 0) or (Index >= FCount) then
    ListIndexError(Index);
  CheckLength(S);
  Changing;
  _PutString(Index, S);
  Changed;
end;

procedure TEditorStrings.PutObject(Index: Integer; AObject: TObject);
begin
  if (Index < 0) or (Index >= FCount) then
    ListIndexError(Index);
  FList[Index].FObject := AObject;
end;

function TEditorStrings.GetObject(Index: Integer): TObject;
begin
  if (Index < 0) or (Index >= FCount) then
    ListIndexError(Index);
  result := FList[Index].FObject;
end;

procedure TEditorStrings.PutParaString(Index: integer; const S: string);
var
  _P, _PI: integer;
  old_count, old_precount: integer;
begin
  if not FMemoEx.FWordWrap then
    Put(Index, S)
  else
  begin
    Index2ParaIndex(Index, _P, _PI);
    if (_P = -1) or (_PI = -1) then
      ListIndexError(Index);
    Changing;
    old_count := FList[_P].FCount;
    old_precount := FList[_P].FPreCount;
    CheckLength(S);
    FList[_P].FStrings[_PI] := S;
    ReformatParagraph(_P);
    if old_count <> FList[_P].FCount then
      Recount(_P)
    else
      FList[_P].FPreCount := old_precount;
    Changed;
  end;
end;

procedure TEditorStrings.SetUpdateState(Updating: Boolean);
begin
  if Updating then
    Changing
  else
    Changed;
end;

procedure TEditorStrings.SetTextStr(const Value: string);
begin
  inc(FMemoEx.FUpdateLock);
  inherited SetTextStr(Value);
  dec(FMemoEx.FUpdateLock);
  if FMemoEx.FUpdateLock = 0 then
  begin
    {$IFDEF MEMOEX_EDITOR}
    FMemoEx.CantUndo;
    {$ENDIF MEMOEX_EDITOR}
    FMemoEx.TextAllChanged;
    FMemoEx.SetCaretInternal(0, 0);
  end;
end;

procedure TEditorStrings.ReLine;
// complete line with spaces if caret X is after real chars
var
  L: integer;
begin
  inc(FMemoEx.FUpdateLock);
  try
    {$IFDEF MEMOEX_UNDO}
    if FParaLinesCount = 0 then
      L := FMemoEx.FCaretX
    else
      L := Length(ParaStrings[FParaLinesCount - 1]);
    while FMemoEx.FCaretY > FParaLinesCount - 1 do
    begin
      if FParaLinesCount > 0 then
        TReLineUndo.Create(FMemoEx, L, FMemoEx.FCaretY, #13#10);
      L := 0;
      Add('');
    end;
    {$ENDIF MEMOEX_UNDO}
    if FMemoEx.FCaretX > Length(ParaStrings[FMemoEx.FCaretY]) then
    begin
      L := FMemoEx.FCaretX - Length(ParaStrings[FMemoEx.FCaretY]);
      {$IFDEF MEMOEX_UNDO}
      TReLineUndo.Create(FMemoEx, Length(ParaStrings[FMemoEx.FCaretY]), FMemoEx.FCaretY, StringOfChar(' ', L));
      {$ENDIF MEMOEX_UNDO}
      PutParaString(FMemoEx.FCaretY, ParaStrings[FMemoEx.FCaretY] + StringOfChar(' ', L));
    end;
  finally
    dec(FMemoEx.FUpdateLock);
  end;
end; { ReLine }

procedure TEditorStrings.SetLockText(const Text: string);
begin
  inc(FMemoEx.FUpdateLock);
  try
    inherited SetTextStr(Text);
  finally
    dec(FMemoEx.FUpdateLock);
  end;
end;

procedure TEditorStrings.SetInternalParaStr(Index: integer; const Value: string);
begin
  inc(FMemoEx.FUpdateLock);
  try
    PutParaString(Index, Value);
  finally
    dec(FMemoEx.FUpdateLock);
  end;
end;

procedure TEditorStrings.SetInternal(Index: integer; const Value: string);
begin
  inc(FMemoEx.FUpdateLock);
  try
    Put(Index, Value);
  finally
    dec(FMemoEx.FUpdateLock);
  end;
end;

function TEditorStrings.GetTextStr: string;
var
  i, sizetot: integer;
  P: PChar;
begin
//  result := inherited GetTextStr; exit; 8 times slower than code below
  SizeTot := GetTextLength;
  Setlength(Result, Sizetot);
  P := Pointer(Result);
  for i := 0 to FCount - 1 do
    with FList[i] do
    begin
      P := StringDynArrayToPChar(pointer(FStrings), FCount, P); // line
      {$ifdef UNICODE}
      pCardinal(P)^ := $000A000D;
      {$else}
      pWord(P)^ := $0A0D;
      {$endif}
      inc(P, 2); // CRLF
    end;
  assert((p - pointer(result)) = sizeTot);
end;

function TEditorStrings.GetCount: Integer;
begin
  result := FCount;
end;

function TEditorStrings.GetTextLength: integer;
var
  i: integer;
begin
  Result := FCount * 2; // CRLF char size
  for i := 0 to FCount - 1 do
    with FList[i] do
      inc(Result, StringDynArrayGetSize(pointer(FStrings), FCount));
end;

function TEditorStrings.HasText: boolean;
var
  i: integer;
begin
  result := true;
  for i := 0 to FCount - 1 do
    with FList[i] do
      if StringDynArrayGetSize(pointer(FStrings), FCount) <> 0 then
        exit;
  result := false;
end;

{ TEditorClient }

function TEditorClient.GetCanvas: TCanvas;
begin
  Result := FMemoEx.Canvas;
end;

function TEditorClient.Left: integer;
begin
  Result := FMemoEx.GutterWidth + GutterRightMargin;
end;

function TEditorClient.Height: integer;
begin
  Result := FMemoEx.ClientHeight;
end;

function TEditorClient.Width: integer;
begin
  Result := Max(FMemoEx.ClientWidth - Left, 0);
end;

function TEditorClient.ClientWidth: integer;
begin
  Result := Width;
end;

function TEditorClient.ClientHeight: integer;
begin
  Result := Height;
end;

function TEditorClient.ClientRect: TRect;
begin
  Result := Bounds(Left, Top, Width, Height);
end;

function TEditorClient.BoundsRect: TRect;
begin
  Result := Bounds(0, 0, Width, Height);
end;

procedure TGutter.Invalidate;
begin
  Paint;
end;

procedure TGutter.Paint;
var
  Rect: TRect;
begin
  with FMemoEx, Canvas do
  begin
    Brush.Style := bsSolid;
    Brush.Color := FGutterColor;
    Rect.Left := 0;
    Rect.Top := EditorClient.Top;
    Rect.Right := GutterWidth;
    Rect.Bottom := Rect.Top + EditorClient.Height;
    FillRect(Rect);
    Pen.Width := 1;
    Pen.Color := Color;
    MoveTo(GutterWidth - 2, EditorClient.Top);
    LineTo(GutterWidth - 2, EditorClient.Top + EditorClient.Height);
    Pen.Width := 2;
    MoveTo(GutterWidth + 1, EditorClient.Top);
    LineTo(GutterWidth + 1, EditorClient.Top + EditorClient.Height);
    Pen.Width := 1;
    Pen.Color := clGray;
    MoveTo(GutterWidth - 1, EditorClient.Top);
    LineTo(GutterWidth - 1, EditorClient.Top + EditorClient.Height);
  end;
  with FMemoEx do
    GutterPaint(Canvas, Rect);
end;

constructor TCustomMemoEx.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := [csCaptureMouse, csClickEvents {, csOpaque}, csDoubleClicks, csReplicatable];
  FInsertMode := true;
  FReadOnly := false;
  FWantTabs := true;
  FLines := TEditorStrings.Create;
  FLines.FMemoEx := Self;
  FKeyboard := TKeyboard.Create;
  FRows := 1;
  FCols := 1;

  {$IFDEF MEMOEX_UNDO}
  FUndoBuffer := TUndoBuffer.Create;
  FUndoBuffer.FMemoEx := Self;
  FGroupUndo := true;
  {$ENDIF MEMOEX_UNDO}
  FDrawBitmap := TBitmap.Create;

  FFont := TFont.Create;
  with FFont do
  begin
    if Screen.Fonts.IndexOf('Consolas')>=0 then
    begin
      Name := 'Consolas';
      Size := 9;
    end
    else
    begin
      Name := 'Courier New';
      Size := 10;
    end;
    Pitch := fpFixed;
    OnChange := FontChanged;
  end;

  FRightMarginVisible := true;
  FRightMargin := 80;
  FBorderStyle := bsSingle;
  Ctl3d := true;
  Height := 40;
  Width := 150;
  ParentColor := false;
  Cursor := crIBeam;
  TabStop := true;
  FTabStops := '8';
  FSmartTab := true;
  FBackSpaceUnindents := true;
  FAutoIndent := true;
  FKeepTrailingBlanks := false;
  FCursorBeyondEOF := false;
  FCursorBeyondEOL := true;

  FWordWrap := true;

  FScrollBars := ssBoth;
  scbHorz := TRAControlScrollBar95.Create;
  scbVert := TRAControlScrollBar95.Create;
  scbVert.Kind := sbVertical;
  scbHorz.OnScroll := ScrollBarScroll;
  scbVert.OnScroll := ScrollBarScroll;

  Color := clWindow;
  FGutterColor := clBtnFace;
  FclSelectBC := clHighLight;
  FclSelectFC := clHighLightText;
  FRightMarginColor := clSilver;

  EditorClient := TEditorClient.Create;
  EditorClient.FMemoEx := Self;
  FGutter := TGutter.Create;
  FGutter.FMemoEx := Self;

  FLeftCol := 0;
  FTopRow := 0;
  FSelected := false;
  FCaretX := 0;
  FCaretY := 0;

  timerScroll := TTimer.Create(Self);
  timerScroll.Enabled := false;
  timerScroll.Interval := 100;
  timerScroll.OnTimer := ScrollTimer;

  SelAttrs_Size := 0;

  FTabPos := nil;
  SetMax_X(512);

  mouse_down := false;
  double_clicked := false;
  mouse_dragged := false;

  {$IFDEF MEMOEX_EDITOR}

  {$IFDEF MEMOEX_DEFLAYOUT}
  FKeyboard.SetDefLayout;
  {$ENDIF MEMOEX_DEFLAYOUT}

  {$IFDEF MEMOEX_COMPLETION}
  FCompletion := TCompletion.Create2(Self);
  {$ENDIF MEMOEX_COMPLETION}

  {$ENDIF MEMOEX_EDITOR}
end;

destructor TCustomMemoEx.Destroy;
begin
  FLines.Free;
  scbHorz.Free;
  scbVert.Free;
  EditorClient.Free;
  FKeyboard.Free;
  {$IFDEF MEMOEX_EDITOR}
  {$IFDEF MEMOEX_COMPLETION}
  FCompletion.Free;
  {$ENDIF MEMOEX_COMPLETION}
  {$ENDIF MEMOEX_EDITOR}
  FGutter.Free;
  FDrawBitmap.Free;
  FFont.Free;
  SelAttrs := nil;
  FTabPos := nil;
  {$IFDEF MEMOEX_UNDO}
  FreeAndNil(FUndoBuffer);
  {$ENDIF MEMOEX_UNDO}
  inherited Destroy;
end;

procedure TCustomMemoEx.Invalidate;
begin
  if (csLoading in ComponentState) then
    exit;
  if FUpdateLock = 0 then
    inherited;
end;

procedure TCustomMemoEx.Loaded;
begin
  inherited Loaded;
  scbVertWidth := GetSystemMetrics(SM_CXVSCROLL);
  scbHorzHeight := GetSystemMetrics(SM_CYHSCROLL);
  NextClipViewer := SetClipboardViewer(Handle);
  UpdateEditorSize;
  Changed;
  SelectionChanged;
  ClipboardChanged;
  FModified := false;
  {$IFDEF MEMOEX_COMPLETION}
  FCompletion.UpdateAutoChange;
  {$ENDIF}
end;

procedure TCustomMemoEx.CreateParams(var Params: TCreateParams);
const
  BorderStyles: array[TBorderStyle] of cardinal = (0, WS_BORDER);
  ScrollStyles: array[TScrollStyle] of cardinal = (0, WS_HSCROLL, WS_VSCROLL, WS_HSCROLL or WS_VSCROLL);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    Style := Style or BorderStyles[FBorderStyle] or ScrollStyles[FScrollBars];
    if NewStyleControls and Ctl3D and (FBorderStyle = bsSingle) then
    begin
      Style := Style and not WS_BORDER;
      ExStyle := ExStyle or WS_EX_CLIENTEDGE;
    end;
    WindowClass.style := WindowClass.style and not (CS_HREDRAW or CS_VREDRAW);
  end;
end;

procedure TCustomMemoEx.Resize;
begin
  if not (csLoading in ComponentState) then
  begin
    UpdateEditorSize(true, false);
    Invalidate;
  end;
end;

procedure TCustomMemoEx.CreateWnd;
begin
  inherited CreateWnd;
  if FScrollBars in [ssHorizontal, ssBoth] then
    scbHorz.Handle := Handle;
  if FScrollBars in [ssVertical, ssBoth] then
    scbVert.Handle := Handle;
  FAllRepaint := true;
end;

procedure TCustomMemoEx.SetBorderStyle(Value: TBorderStyle);
begin
  if FBorderStyle <> Value then
  begin
    FBorderStyle := Value;
    RecreateWnd;
  end;
end;

procedure TCustomMemoEx.PaintSelection;
var
  iR: integer;
begin
  for iR := FUpdateSelBegY to FUpdateSelEndY do
    PaintLine(iR, -1, -1);
end;

procedure TCustomMemoEx.SetUnSelected;
begin
  if FSelected then
  begin
    FSelectedText := false;
    {$IFDEF MEMOEX_UNDO}
    TUnselectUndo.Create(Self, FCaretX, FCaretY, FSelBlock, FSelBegX, FSelBegY, FSelEndX, FSelEndY);
    {$ENDIF MEMOEX_UNDO}
    PaintSelection;
  end;
end;

function IsRectEmpty(R: TRect): boolean;
begin
  Result := (R.Top = R.Bottom) and (R.Left = R.Right);
end;

function TCustomMemoEx.CalcCellRect(const X, Y: integer): TRect;
begin
  Result := Bounds(EditorClient.Left + X * FCellRect.Width + 1,
    EditorClient.Top + Y * FCellRect.Height, FCellRect.Width, FCellRect.Height)
end;

procedure TCustomMemoEx.Paint;
var
  iR: integer;
  ECR: TRect;
  BX, EX, BY, EY: integer;
begin
  if FUpdateLock > 0 then
    exit;
  {$IFDEF MEMOEX_NOOPTIMIZE}
  FAllRepaint := true;
  {$ENDIF}
  PaintCaret(false);
  ECR := EditorClient.Canvas.ClipRect;
  OffsetRect(ECR, -FGutterWidth, 0);
  if FAllRepaint then
    ECR := EditorClient.BoundsRect;
  BX := ECR.Left div FCellRect.Width - 1;
  EX := ECR.Right div FCellRect.Width + 1;
  BY := ECR.Top div FCellRect.Height;
  EY := ECR.Bottom div FCellRect.Height + 1;
  for iR := BY to EY do
    PaintLine(FTopRow + iR, FLeftCol + BX, FLeftCol + EX);
  PaintCaret(true);
  FGutter.Paint;
  FAllRepaint := false;
end;

procedure TCustomMemoEx.BeginUpdate;
begin
  if self = nil then
    exit;
  inc(FUpdateLock);
end;

procedure TCustomMemoEx.EndUpdate;
begin
  if self = nil then
    exit;
  if FUpdateLock = 0 then
    Exit; { Error ? }
  dec(FUpdateLock);
  if FUpdateLock = 0 then
  begin
    FAllRepaint := true;
    UpdateEditorSize(false);
    Changed;
    StatusChanged;
    Invalidate;
  end;
end;

{ FTabPos... }
procedure TCustomMemoEx.SetMax_X(const Value: integer);
begin
  Max_X := Value;
  SetLength(FTabPos, Max_X);
end;

{ FullUpdate }
procedure TCustomMemoEx.UpdateEditorSize(const FullUpdate: boolean; const RepaintGutter: boolean);
const
  BiggestSymbol = 'W';
var
  Size: TSize;
begin
  if (csLoading in ComponentState) then
    exit;
  if FullUpdate then
  begin
    EditorClient.Canvas.Font := Font;
    EditorClient.Canvas.Font.Style := [fsBold, fsItalic];
    Size := EditorClient.Canvas.TextExtent(BiggestSymbol);
    FCellRect.Width := Max(1, Size.cx);
    FCellRect.Height := Max(1, Size.cy);
    EditorClient.Canvas.Font := Font;
    Size := EditorClient.Canvas.TextExtent(BiggestSymbol);
    {
    if FCellRect.Width <> Max(1, Size.cx) then
      raise EMemoExError.CreateFmt('Font %s has inconsistent width vs style', [Font.Name]);
    }
    FDrawBitmap.Canvas.Font.Assign(Font);
    FDrawBitmap.Canvas.Brush.Assign(EditorClient.Canvas.Brush);
    FDrawBitmap.Width := Width;
    FDrawBitmap.Height := FCellRect.Height;
  end;
  FVisibleColCount := Trunc(EditorClient.ClientWidth / FCellRect.Width);
  FVisibleRowCount := Trunc(EditorClient.ClientHeight / FCellRect.Height);
  FLastVisibleCol := FLeftCol + FVisibleColCount - 1;
  FLastVisibleRow := FTopRow + FVisibleRowCount - 1;
  FCols := -1;
  FRows := -1;
  Rows := FLines.ParaLineCount;
  if FWordWrap then
    Cols := FRightMargin
  else
    Cols := Max_X;
  if RepaintGutter then
    FGutter.Invalidate;
end;

procedure TCustomMemoEx.PaintLine(const Line: integer; ColBeg, ColEnd: integer);
var
  Ch: string;
  R: TRect;
  F, k, x, i, j, iC, jC, SL, MX, PX, PY: integer;
  T, S: string;
  FAttrs: TLineAttrs;
  LA, LB: TLineAttr;
begin
  if (Line < FTopRow) or (Line > FTopRow + FVisibleRowCount) or (FUpdateLock > 0) then
    exit;
  if ColBeg < FLeftCol then
    ColBeg := FLeftCol;
  if (ColEnd < 0) or (ColEnd > FLeftCol + FVisibleColCount) then
    ColEnd := FLeftCol + FVisibleColCount;
  ColEnd := Min(ColEnd, Max_X - 1);
  j := 0;
  i := ColBeg;
  if (Line > -1) and (Line < FLines.ParaLineCount) then
    with FDrawBitmap do
    begin
      T := FLines.GetParagraphByIndex(Line, PY, PX);
      S := FLines.ParaStrings[Line];
      if not FWordWrap then
      begin
        iC := ColBeg;
        jC := ColEnd;
      end
      else
      begin
        iC := 0;
        jC := length(T);
      end;
      GetLineAttr(PY, Line, PX, length(S), iC, jC, T, FAttrs);
      Canvas.Brush.Color := Color;
      Canvas.FillRect(Bounds(EditorClient.Left, 0, 1, FCellRect.Height));

      SL := Length(S);
      if SL > ColEnd then
        MX := ColEnd
      else
        MX := SL;

      if FStripInvisible and FReadOnly and (ColBeg > 0) then
      begin
        x := PX + ColBeg;
        if x >= SelAttrs_Size then
          x := SelAttrs_Size - 1;
        for k := PX to x do
          if FAttrs[k].FC = FAttrs[k].BC then
            inc(j);
      end;

      while i < MX do
        with Canvas do
        begin
          iC := i + 1;
          jC := iC + 1;
          if iC <= SL then
            Ch := S[iC]
          else
            Ch := ' ';
          if (iC + PX > SelAttrs_Size) or (jC + PX > SelAttrs_Size) then
            break;
          LA := FAttrs[iC + PX - 1];
          if SelAttrs[iC + PX - 1] then
            with LA do
            begin
              FC := FclSelectFC;
              BC := FclSelectBC;
            end;
          while (jC <= MX) and (jC + PX <= SelAttrs_Size) do
          begin
            // append chars with same attrs into Ch
            LB := FAttrs[jC + PX - 1];
            if SelAttrs[jC + PX - 1] then
              with LB do
              begin
                FC := FclSelectFC;
                BC := FclSelectBC;
              end;
            if (LA.FC = LB.FC) and (LA.BC = LB.BC) and (LA.LastInteger = LB.LastInteger) then
            begin
              if jC <= SL then
                Ch := Ch + S[jC]
              else
                Ch := Ch + ' ';
              inc(jC);
            end
            else
              break;
          end;
          if (not ((LA.BC = LA.FC) and FStripInvisible)) or not readonly then
          begin
            // draw Ch into bitmap
            Brush.Color := LA.BC;
            Font.Color := LA.FC;
            Font.Style := LA.Style;
            R.Left := EditorClient.Left + (i - FLeftCol - j) * FCellRect.Width + 1;
            R.Top := 0;
            R.Right := R.Left + FCellRect.Width * Length(Ch);
            R.Bottom := FCellRect.Height;
            FillRect(R);
            TextOut(R.Left, 0, Ch);
          end
          else
            inc(j, length(Ch));
          i := jC - 1;
        end;
    end
  else
  begin
    FDrawBitmap.Canvas.Brush.Color := Color;
    FDrawBitmap.Canvas.FillRect(Bounds(EditorClient.Left, 0, 1, FCellRect.Height));
  end;

  R := Bounds(CalcCellRect(i - j - FLeftCol, Line - FTopRow).Left, 0,
    (FLeftCol + FVisibleColCount - i{ - j}  + 10) * FCellRect.Width, FCellRect.Height);
  FDrawBitmap.Canvas.Brush.Color := Color;
  FDrawBitmap.Canvas.FillRect(R);

  R := Bounds(EditorClient.Left, (Line - FTopRow) * FCellRect.Height,
    (FVisibleColCount + 2) * FCellRect.Width, FCellRect.Height);

  if FRightMarginVisible and (FRightMargin > FLeftCol) and (FRightMargin < FLastVisibleCol + 3) then
    with FDrawBitmap.Canvas do
    begin
      Pen.Color := FRightMarginColor;
      F := CalcCellRect(FRightMargin - FLeftCol, 0).Left;
      MoveTo(F, 0);
      LineTo(F, FCellRect.Height);
    end;

  BitBlt(EditorClient.Canvas.Handle, R.Left, R.Top, R.Right - R.Left, FCellRect.Height,
    FDrawBitmap.Canvas.Handle, R.Left, 0, SRCCOPY);
end;

procedure TCustomMemoEx.GetLineAttr(Line, LineIdx, LineOffs, LineLen, ColBeg, ColEnd: integer;
  const ALine: string; var FAttrs: TLineAttrs);

  procedure SetAttrs(A: pLineAttr; count: integer);
  var
    i: integer;
  begin
    for i := 1 to Count do
    begin
      A^.FC := clWindowText;
      A^.BC := clWindow;
      A^.LastInteger := 0;
      inc(A);
    end;
  end;

  procedure ChangeSelectedAttr;

    procedure DoChange(const iBeg, iEnd: integer);
    var
      i: integer;
    begin
      if (iBeg + LineOffs < SelAttrs_Size) and (iEnd + LineOffs < SelAttrs_Size) then
        for i := iBeg + LineOffs to iEnd + LineOffs do
          SelAttrs[i] := true;
    end;

  begin
    if SelAttrs_Size > 0 then
      FillChar(SelAttrs[0], SelAttrs_Size, 0);
    if not FSelected then
      exit;
    if (LineIdx = FSelBegY) and (LineIdx = FSelEndY) then
      DoChange(FSelBegX, Min(LineLen - 1, FSelEndX - 1 + integer(FInclusive)))
    else
    begin
      if LineIdx = FSelBegY then
        DoChange(FSelBegX, LineLen - 1);
      if (LineIdx > FSelBegY) and (LineIdx < FSelEndY) then
        DoChange(0, LineLen - 1);
      if LineIdx = FSelEndY then
        DoChange(0, Min(LineLen - 1, FSelEndX - 1 + integer(FInclusive)));
    end
  end;

begin
  if SelAttrs_Size <> length(ALine) + 1 then
  begin
    SelAttrs_Size := length(ALine) + 1;
    SetLength(SelAttrs, SelAttrs_Size);
  end;
  ChangeSelectedAttr;

  SetLength(FAttrs, SelAttrs_Size);
  SetAttrs(@FAttrs[0], SelAttrs_Size);

  if (ALine <> '') and Assigned(FOnGetLineAttr) then
    FOnGetLineAttr(Self, ALine, Line, SelAttrs, FAttrs);
end;

procedure TCustomMemoEx.ScrollBarScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: integer);
begin
  case ScrollCode of
    scLineUp..scPageDown, scTrack:
      if Sender = scbVert then
        Scroll(true, ScrollPos)
      else if Sender = scbHorz then
        Scroll(false, ScrollPos);
  end;
end;

procedure TCustomMemoEx.Scroll(const Vert: boolean; const ScrollPos: integer);
var
  R, RClip, RUpdate: TRect;
  OldFTopRow: integer;
begin
  if FUpdateLock = 0 then
  begin
    PaintCaret(false);
    if Vert then
    begin {Vertical Scroll}
      OldFTopRow := FTopRow;
      FTopRow := ScrollPos;
      if Abs((OldFTopRow - ScrollPos) * FCellRect.Height) < EditorClient.Height then
      begin
        R := EditorClient.ClientRect;
        R.Bottom := R.Top + CellRect.Height * (FVisibleRowCount + 1); {??}
        RClip := R;
        ScrollDC(EditorClient.Canvas.Handle,            // handle of device context
          0,                                            // horizontal scroll units
          (OldFTopRow - ScrollPos) * FCellRect.Height,  // vertical scroll units
          R,                                            // address of structure for scrolling rectangle
          RClip,                                        // address of structure for clipping rectangle
          0,                                            // handle of scrolling region
          @RUpdate                                      // address of structure for update rectangle
        );
        InvalidateRect(Handle, @RUpdate, false);
        if Assigned(OnPaintGutter) then
          Gutter.Paint;
      end
      else
        Invalidate;
      Update;
    end
    else {Horizontal Scroll}
    begin
      FLeftCol := ScrollPos;
      Invalidate;
    end;
  end
  else { FUpdateLock > 0 }
  begin
    if Vert then
      FTopRow := ScrollPos
    else
      FLeftCol := ScrollPos;
  end;
  FLastVisibleRow := FTopRow + FVisibleRowCount - 1;
  FLastVisibleCol := FLeftCol + FVisibleColCount - 1;
  if FUpdateLock = 0 then
  begin
    // DrawRightMargin;
    PaintCaret(true);
  end;
  if Assigned(FOnScroll) then
    FOnScroll(Self);
end;

procedure TCustomMemoEx.PaintCaret(bShow: boolean);
begin
  if not bShow then
    HideCaret(Handle)
  else if Focused then
  begin
    with CalcCellRect(FCaretX - FLeftCol, FCaretY - FTopRow) do
      SetCaretPos(Left - 1, Top + 1);
    ShowCaret(Handle)
  end;
end;

procedure TCustomMemoEx.SetCaretInternal(X, Y: integer);
begin
  if (X = FCaretX) and (Y = FCaretY) then
    exit;
  if not FCursorBeyondEOF then
    Y := Min(Y, FLines.ParaLineCount - 1);
  Y := Max(Y, 0);
  X := Min(X, Max_X);
  X := Max(X, 0);
  if Y < FTopRow then
    SetLeftTop(FLeftCol, Y)
  else if Y > Max(FLastVisibleRow, 0) then
    SetLeftTop(FLeftCol, Y - FVisibleRowCount + 1);
  if X < 0 then
    X := 0;
  if X < FLeftCol then
    SetLeftTop(X, FTopRow)
  else if X > FLastVisibleCol then
    SetLeftTop(X - FVisibleColCount {+ 1}, FTopRow);
  with CalcCellRect(X - FLeftCol, Y - FTopRow) do
    SetCaretPos(Left - 1, Top + 1);
  if (FCaretX <> X) or (FCaretY <> Y) then
  begin
    FCaretX := X;
    FCaretY := Y;
    if Assigned(OnSetCaretPos) then
      OnSetCaretPos(Self, X, Y);
    StatusChanged;
  end;
  FCaretX := X;
  FCaretY := Y;
end;

procedure TCustomMemoEx.SetCaret(const X, Y: integer);
begin
  if (X = FCaretX) and (Y = FCaretY) then
    exit;
  {$IFDEF MEMOEX_UNDO}
  TCaretUndo.Create(Self, FCaretX, FCaretY);
  {$ENDIF MEMOEX_UNDO}
  SetCaretInternal(X, Y);
  if FUpdateLock = 0 then
    StatusChanged;
end;

procedure TCustomMemoEx.SetCaretAtParaPos(const ParaIndex, IndexOffs: integer);
var
  X, Y: integer;
begin
  Lines.Paragraph2Caret(ParaIndex, IndexOffs, X, Y);
  SetCaret(X, Y);
end;

procedure TCustomMemoEx.SetCaretPosition(const index, Pos: integer);
begin
  if index = 0 then
    SetCaret(Pos, FCaretY)
  else
    SetCaret(FCaretX, Pos)
end;

procedure TCustomMemoEx.KeyDown(var Key: Word; Shift: TShiftState);
var
  Form: TCustomForm;
{$IFDEF MEMOEX_EDITOR}
  Com: word;
{$ENDIF MEMOEX_EDITOR}
begin
  {$IFDEF MEMOEX_COMPLETION}
  if FCompletion.FVisible then begin
    if FCompletion.DoKeyDown(Key, Shift) then
      exit;
  end
  else
    FCompletion.FTimer.Enabled := false;
  {$ENDIF MEMOEX_COMPLETION}
  if (Key = VK_TAB) and ((Shift = []) or (Shift = [ssShift])) then
    if FReadOnly or (not FWantTabs) then
    begin
      Form := GetParentForm(Self);
      if Assigned(Form) then
      begin
        Key := 0;
        if Shift = [] then
          Form.Perform(WM_NEXTDLGCTL, 0, 0)
        else
          Form.Perform(WM_NEXTDLGCTL, 1, 0);
      end;
      exit;
    end;

  {$IFDEF MEMOEX_EDITOR}
  if WaitSecondKey then
  begin
    Com := FKeyboard.Command2(Key1, Shift1, Key, Shift);
    WaitSecondKey := false;
    IgnoreKeyPress := true;
  end
  else
  begin
    inherited KeyDown(Key, Shift);
    Key1 := Key;
    Shift1 := Shift;
    Com := FKeyboard.Command(Key, Shift);
    if Com = twoKeyCommand then
    begin
      IgnoreKeyPress := true;
      WaitSecondKey := true;
    end
    else
      IgnoreKeyPress := Com > 0;
  end;
  if (Com > 0) and (Com <> twoKeyCommand) then
  begin
    Key := 0;
    Shift := [];
    Command(Com);
  end;
  {$IFDEF MEMOEX_COMPLETION}
  if (Com = ecBackSpace) then
    FCompletion.DoKeyPress(#8);
  {$ENDIF MEMOEX_COMPLETION}
  {$ENDIF MEMOEX_EDITOR}
end;

{$IFDEF MEMOEX_EDITOR}

procedure TCustomMemoEx.ReLine;
begin
  FLines.ReLine;
end;

procedure TCustomMemoEx.KeyPress(var Key: Char);
begin
  if IgnoreKeyPress then
  begin
    IgnoreKeyPress := false;
    exit
  end;
  if FReadOnly then
    exit;
  PaintCaret(false);
  inherited KeyPress(Key);

  Command(ord(Key));

  PaintCaret(true);
end;

function AutoChangeCompare(Item1, Item2: pointer): integer;
var
  i, j: integer;
begin
  i := length(PAutoChangeWord(Item1)^.OldWord);
  j := length(PAutoChangeWord(Item2)^.OldWord);
  if i = j then
    Result := 0
  else if i > j then
    Result := 1
  else
    Result := -1;
end;

procedure TCustomMemoEx.InsertChar(const Key: Char);

  {$IFDEF MEMOEX_COMPLETION}
  function GetAutoChangeWord(const CurrentWord: string; var NewWord: string): boolean;
  var
    i, j, k: integer;
    s, t: string;
  begin
    Result := false;
    t := DoChangeCase(CurrentWord, RA_CASE_CONVERT_LOWER);
    j := length(t);
    for i := 0 to FCompletion.FAutoChangeList.Count - 1 do
    begin
      s := PAutoChangeWord(FCompletion.FAutoChangeList[i])^.OldWord;
      k := length(s);
      if j < k then
        break
      else if j = k then
        if t = s then
        begin
          Result := true;
          NewWord := PAutoChangeWord(FCompletion.FAutoChangeList[i])^.NewWord;
          break;
        end;
    end;
  end;
  {$ENDIF}
var
  S: string;
  {$IFDEF MEMOEX_COMPLETION}
  T, old_str, new_str: string;
  k1, k2, str_pos: integer;
  AutoChanged, AddKeyToNewStr: boolean;
  {$ENDIF}
  oldChar: string;
  i, _X, _Y, Y: integer;
  b: boolean;
  KeyAnsiChar: AnsiChar;
begin
  ReLine;
  if Key < #32 then
    exit;
  {$ifdef UNICODE}
  if Key > #255 then
    KeyAnsiChar := #255
  else
  {$endif}
    KeyAnsiChar := AnsiChar(Key);
  {$IFDEF MEMOEX_COMPLETION}
  if not (KeyAnsiChar in RAEditorCompletionChars) then
    FCompletion.DoKeyPress(Key);
  {$ENDIF MEMOEX_COMPLETION}
  DeleteSelected;
  FLines.Caret2Paragraph(FCaretX, FCaretY, FParaY, FParaX);
  {$IFDEF MEMOEX_COMPLETION}
  str_pos := 0;
  if (KeyAnsiChar in _AutoChangePunctuation) and (FCompletion.FAutoChangeList.Count > 0) then
  begin
    S := FLines[FParaY];
    AutoChanged := false;
    AddKeyToNewStr := false;
    str_pos := FParaX - 1;
    k1 := length(PAutoChangeWord(FCompletion.FAutoChangeList[0])^.OldWord);
    k2 := length(PAutoChangeWord(FCompletion.FAutoChangeList[FCompletion.FAutoChangeList.Count - 1])^.OldWord);
    while (str_pos > -1) and (FParaX - str_pos <= k2) do
    begin
      if FParaX - str_pos >= k1 then
      begin
        old_str := System.Copy(S, str_pos + 1, FParaX - str_pos);
        AutoChanged := GetAutoChangeWord(old_str, new_str);
        if not AutoChanged then
          AutoChanged := GetAutoChangeWord(old_str + Key, new_str)
        else
          AddKeyToNewStr := true;
        if AutoChanged then
          if ((str_pos > 0) and ({$ifdef UNICODE}(S[str_pos] < #255) and {$endif}
            (AnsiChar(S[str_pos]) in _AutoChangePunctuation))) or (str_pos = 0) then
            break
          else
            AutoChanged := false;
      end;
      dec(str_pos);
    end;
    if AutoChanged then
      if AddKeyToNewStr then
        if GetAutoChangeWord(Key, T) then
          new_str := new_str + T
        else
          new_str := new_str + Key
      else

    else
    begin
      AutoChanged := GetAutoChangeWord(Key, new_str);
      if AutoChanged then
      begin
        str_pos := FParaX;
        old_str := '';
      end;
    end;
  end
  else
    AutoChanged := false;
  if AutoChanged then
  begin
    {$IFDEF MEMOEX_UNDO}
    //  undo
    BeginCompound;
    TCaretUndo.Create(Self, FCaretX, FCaretY);
    FLines.Paragraph2Caret(FParaY, str_pos, _X, _Y);
    if (length(old_str) + integer(not FInsertMode) > 0) and (length(S) > 0) then
    begin
      if FInsertMode then
        T := old_str
      else
        T := old_str + S[FParaX + 1];
      TDeleteUndo.Create(Self, _X, _Y, T);
    end;
    if length(new_str) > 0 then
      TInsertUndo.Create(Self, _X, _Y, new_str);
    EndCompound;
    {$ENDIF}
    k1 := FLines.Paragraphs[FParaY].FCount;
    System.Delete(S, str_pos + 1, length(old_str) + integer(not FInsertMode));
    System.Insert(new_str, S, str_pos + 1);
    FLines.Internal[FParaY] := S;
    B := k1 <> FLines.Paragraphs[FParaY].FCount;
    FParaX := str_pos + length(new_str);
  end
  else
  {$ENDIF MEMOEX_COMPLETION}
  begin
    S := FLines.ParaStrings[FCaretY];
    if FInsertMode then
    begin
      {$IFDEF MEMOEX_UNDO}
      TInsertUndo.Create(Self, FCaretX, FCaretY, Key);
      {$ENDIF}
      Insert(Key, S, FCaretX + 1);
    end
    else
    begin
      if FCaretX + 1 <= Length(S) then
      begin
        oldChar := S[FCaretX + 1];
        S[FCaretX + 1] := Key;
      end
      else
      begin
        oldChar := '';
        S := S + Key;
      end;
          {$IFDEF MEMOEX_UNDO}
      TOverwriteUndo.Create(Self, FCaretX, FCaretY, oldChar, Key);
          {$ENDIF MEMOEX_UNDO}
    end;

    Y := FCaretY;
    i := FLines.Paragraphs[FParaY].FCount;
    FLines.InternalParaStrings[Y] := S;
    inc(FParaX);

    B := i <> FLines.Paragraphs[FParaY].FCount;
  end;

  i := RepaintParagraph(FCaretY);

  if B then
  begin
    UpdateEditorSize(false);
    RedrawFrom(i + 1);
  end;

  FLines.Paragraph2Caret(FParaY, FParaX, _X, _Y);
  SetCaretInternal(_X, _Y);

  Changed;

  {$IFDEF MEMOEX_COMPLETION}
  if KeyAnsiChar in RAEditorCompletionChars then
    FCompletion.DoKeyPress(Key);
  {$ENDIF MEMOEX_COMPLETION}
end;

{$ENDIF MEMOEX_EDITOR}

procedure TCustomMemoEx.RedrawFrom(YFrom: integer);
var
  i: integer;
begin
  for i := YFrom - 1 to FLastVisibleRow + 1 do
    PaintLine(i, -1, -1);
end;

function TCustomMemoEx.RepaintParagraph(LineIndex: integer): integer;
var
  P, PI, i, j, k: integer;
begin
  FLines.Index2ParaIndex(LineIndex, P, PI);
  j := LineIndex - PI;
  k := j + FLines.FList[P].FCount - 1;
  if j < FTopRow - 1 then
    j := FTopRow - 1;
  j := Max(0, j);
  if k > FLastVisibleRow + 1 then
    k := FLastVisibleRow + 1;
  Result := k;
  for i := j to k do
    PaintLine(i, -1, -1);
end;

function TCustomMemoEx.IsUndoEmpty: boolean;
begin
{$IFDEF MEMOEX_EDITOR}   Result := FUndoBuffer.FPtr < 0; {$endif}
end;

function TCustomMemoEx.YinBounds(AY: integer): boolean;
begin
  Result := (AY > -1) and (AY < FLines.ParaLineCount);
end;

function TCustomMemoEx.DoChangeCase(const st: string; Conversion: byte): string;
begin
  if Assigned(FOnCaseConversion) then
    Result := FOnCaseConversion(Self, Conversion, st)
  else
    case Conversion of
      RA_CASE_CONVERT_UPPER:
        Result := ANSIUpperCase(st);
      RA_CASE_CONVERT_LOWER:
        Result := ANSILowerCase(st);
    else
      Result := ANSIChangeCase(st);
    end;
end;

{$IFDEF MEMOEX_EDITOR}

procedure TCustomMemoEx.Command(ACommand: TEditCommand);
var
  X, Y: integer;
  {$IFDEF MEMOEX_UNDO}
  CaretUndo: boolean;
  {$ENDIF MEMOEX_UNDO}
type
  TPr = procedure of object;

  procedure DoAndCorrectXY(Pr: TPr);
  begin
    Pr;
    X := FCaretX;
    Y := FCaretY;
    {$IFDEF MEMOEX_COMPLETION}
    CaretUndo := false;
    {$ENDIF MEMOEX_COMPLETION}
  end;

  function Com(const Args: array of TEditCommand): boolean;
  var
    i: integer;
  begin
    Result := true;
    for i := 0 to High(Args) do
      if Args[i] = ACommand then
        exit;
    Result := false;
  end;

  procedure SetSel1(X, Y: integer);
  begin
    SetSel(X, Y);
    {$IFDEF MEMOEX_UNDO}
    CaretUndo := false;
    {$ENDIF MEMOEX_UNDO}
  end;

  procedure SetSelText1(S: string);
  begin
    SelText := S;
    CaretUndo := false;
  end;

  procedure Complete;
  begin
    raise EComplete.Create('');
  end;

var
  F, _Y: integer;
  S, S2, T: string;
  B: boolean;
  iBeg, iEnd: integer;
begin
  X := FCaretX;
  Y := FCaretY;

  {$IFDEF MEMOEX_UNDO}
  CaretUndo := true;
  {$ENDIF MEMOEX_UNDO}
  PaintCaret(false);

 { macro recording }
  if FRecording and not Com([ecRecordMacro, ecBeginCompound]) and (FCompound = 0) then
    FMacro := FMacro + Char(Lo(ACommand)) + Char(Hi(ACommand));

  try
    try
      case ACommand of

        { caret movements }

        ecLeft, ecRight, ecSelLeft, ecSelRight:
          begin
            if Com([ecSelLeft, ecSelRight]) and not FSelected then
              SetSel1(X, Y);
            B := Com([ecLeft, ecSelLeft]);
            if B then
              dec(X)
            else
              inc(X);
            if (not FCursorBeyondEOL) and (YinBounds(Y)) then
            begin
              _Y := 0;
              if (B) and (X < 0) then
                _Y := -1
              else if (not B) and (X > length(FLines.ParaStrings[Y])) then
                _Y := 1;
              if (_Y <> 0) and (YinBounds(Y + _Y)) then
              begin
                Y := Y + _Y;
                if B then
                  X := length(FLines.ParaStrings[Y])
                else
                  X := 0;
              end
              else if X > length(FLines.ParaStrings[Y]) then
                X := length(FLines.ParaStrings[Y]);
            end
            else if not CursorBeyondEOL then
              X := 0;
            if Com([ecSelLeft, ecSelRight]) then
              SetSel1(X, Y)
            else
              SetUnSelected;
          end;
        ecUp, ecDown, ecSelUp, ecSelDown:
          if (Com([ecUp, ecSelUp]) and (Y > 0)) or
             (Com([ecDown, ecSelDown]) and ((Y < FRows - 1) or (FCursorBeyondEOF))) then
          begin
            if Com([ecSelUp, ecSelDown]) and not FSelected then
              SetSel1(X, Y);
            if Com([ecUp, ecSelUp]) then
              dec(Y)
            else
              inc(Y);
            if (not FCursorBeyondEOL) and (YinBounds(Y)) then
              if X > length(FLines.ParaStrings[Y]) then
                X := length(FLines.ParaStrings[Y]);
            if Com([ecSelUp, ecSelDown]) then
              SetSel1(X, Y)
            else
              SetUnSelected;
          end;
        ecPrevWord, ecSelPrevWord:
          if FLines.ParaLineCount > 0 then
          begin
            S := FLines.ParaStrings[Y];
            if X > length(S) then
              X := length(S);
            if X = 0 then
              if Y > 0 then
              begin
                dec(Y);
                X := length(FLines.ParaStrings[Y]);
              end
              else

            else
            begin
              if (ACommand = ecSelPrevWord) and not FSelected then
                SetSel1(FCaretX, FCaretY);
              B := false;
              for F := X - 1 downto 0 do
                if B then
                  if {$ifdef UNICODE} (S[F + 1] < #255) and {$endif} (AnsiChar(S[F + 1]) in Separators) then
                  begin
                    X := F + 1;
                    break;
                  end
                  else

                else if {$ifdef UNICODE} (S[F + 1] > #255) or {$endif} not (AnsiChar(S[F + 1]) in Separators) then
                  B := true;
              if X = FCaretX then
                X := 0;
              if ACommand = ecSelPrevWord then
                SetSel1(X, Y)
              else
                SetUnselected;
              if (not B) and (X = 0) and (Y > 0) then
              begin
                FCaretX := X;
                Command(ACommand);
                Complete;
              end;
            end;
          end;
        ecNextWord, ecSelNextWord:
          if FLines.ParaLineCount > 0 then
          begin
            if X >= length(FLines.ParaStrings[Y]) then
            begin
              if Y < FLines.ParaLineCount - 1 then
              begin
                inc(Y);
                X := 0;
                if length(FLines.ParaStrings[Y]) > 0 then
                  if FLines.ParaStrings[Y][X + 1] = #32 then
                  begin
                    FCaretX := X;
                    FCaretY := Y;
                    Command(ACommand);
                    Complete;
                  end;
              end;
            end
            else
            begin
              if (ACommand = ecSelNextWord) and not FSelected then
                SetSel1(FCaretX, FCaretY);
              S := FLines.ParaStrings[Y];
              B := false;
              for F := X to Length(S) - 1 do
                if B then
                  if {$ifdef UNICODE} (S[F + 1] > #255) or {$endif} not (AnsiChar(S[F + 1]) in Separators) then
                  begin
                    X := F;
                    break;
                  end
                  else

                else if {$ifdef UNICODE} (S[F + 1] < #255) and {$endif} (AnsiChar(S[F + 1]) in Separators) then
                  B := true;
              if X = FCaretX then
              begin
                B := X <> length(S);
                X := length(S);
              end;
              if ACommand = ecSelNextWord then
                SetSel1(X, Y)
              else
                SetUnselected;
              if (not B) and (X = length(S)) and (Y < FLines.ParaLineCount - 1) then
              begin
                FCaretX := X;
                Command(ACommand);
                Complete;
              end;
            end;
          end;
        ecScrollLineUp, ecScrollLineDown, ecScrollPageUp, ecScrollPageDown:
          begin
            if not ((ACommand = ecScrollLineDown) and (Y >= FLines.ParaLineCount - 1) and (Y = FTopRow)) then
            begin
              case ACommand of
                ecScrollLineUp:
                  F := -1;
                ecScrollLineDown:
                  F := 1;
                ecScrollPageUp:
                  F := -scbVert.LargeChange;
              else
                F := scbVert.LargeChange;
              end;
              scbVert.Position := scbVert.Position + F;
              Scroll(true, scbVert.Position);
            end;
            if Y < FTopRow then
              Y := FTopRow
            else if Y > FLastVisibleRow then
              Y := FLastVisibleRow;
          end;
        ecBeginLine, ecSelBeginLine, ecBeginDoc, ecSelBeginDoc, ecEndLine,
        ecSelEndLine, ecEndDoc, ecSelEndDoc:
          begin
            if Com([ecSelBeginLine, ecSelBeginDoc, ecSelEndLine, ecSelEndDoc]) and not FSelected then
              SetSel1(FCaretX, Y);
            if Com([ecBeginLine, ecSelBeginLine]) then
              X := 0
            else if Com([ecBeginDoc, ecSelBeginDoc]) then
            begin
              X := 0;
              Y := 0;
              SetLeftTop(0, 0);
            end
            else if Com([ecEndLine, ecSelEndLine]) then
            begin
              if FLines.ParaLineCount > 0 then
                X := Length(FLines.ParaStrings[Y])
              else
                X := 0;
            end
            else if Com([ecEndDoc, ecSelEndDoc]) then
              if FLines.ParaLineCount > 0 then
              begin
                Y := FLines.ParaLineCount - 1;
                X := Length(FLines.ParaStrings[Y]);
                SetLeftTop(X - FVisibleColCount, Y - FVisibleRowCount + 1{ div 2});
              end;
            if Com([ecSelBeginLine, ecSelBeginDoc, ecSelEndLine, ecSelEndDoc]) then
              SetSel1(X, Y)
            else
              SetUnSelected;
          end;
        ecPrevPage:
          begin
            scbVert.Position := scbVert.Position - scbVert.LargeChange;
            Scroll(true, scbVert.Position);
            Y := Y - FVisibleRowCount;
            SetUnSelected;
          end;
        ecNextPage:
          begin
            scbVert.Position := scbVert.Position + scbVert.LargeChange;
            Scroll(true, scbVert.Position);
            Y := Y + FVisibleRowCount;
            SetUnSelected;
          end;
        ecSelPrevPage:
          begin
            BeginUpdate;
            SetSel1(X, Y);
            scbVert.Position := scbVert.Position - scbVert.LargeChange;
            Scroll(true, scbVert.Position);
            Y := Y - FVisibleRowCount;
            SetSel1(X, Y);
            EndUpdate;
          end;
        ecSelNextPage:
          begin
            BeginUpdate;
            SetSel1(X, Y);
            scbVert.Position := scbVert.Position + scbVert.LargeChange;
            Scroll(true, scbVert.Position);
            Y := Y + FVisibleRowCount;
            if Y <= FLines.ParaLineCount - 1 then
              SetSel1(X, Y)
            else
              SetSel1(X, FLines.ParaLineCount - 1);
            EndUpdate;
          end;
        ecSelWord:
          if not FSelected and
             (GetWordOnPosEx(FLines.ParaStrings[Y] + ' ', X + 1, iBeg, iEnd) <> '') then
          begin
            SetSel1(iBeg - 1, Y);
            SetSel1(iEnd - 1, Y);
            X := iEnd - 1;
          end;

        ecWindowTop:
          begin
            Y := FTopRow;
            if (not FCursorBeyondEOL) and (YinBounds(Y)) then
              if X > length(FLines.ParaStrings[Y]) then
                X := length(FLines.ParaStrings[Y]);
            SetUnSelected;
          end;
        ecWindowBottom:
          begin
            Y := FTopRow + FVisibleRowCount - 1;
            if (not FCursorBeyondEOL) and (YinBounds(Y)) then
              if X > length(FLines.ParaStrings[Y]) then
                X := length(FLines.ParaStrings[Y]);
            SetUnSelected;
          end;

        { editing }

        {$IFDEF MEMOEX_EDITOR}
        ecCharFirst..ecCharLast:
          if not FReadOnly then
          begin
            InsertChar(Char(ACommand - ecCharFirst));
//            Changed; // AB
            Complete;
          end;
        ecInsertPara:
          if not FReadOnly then
          begin
            DeleteSelected;
            ReLine;
            FLines.Caret2Paragraph(X, Y, FParaY, FParaX);
            S := FLines[FParaY];
            S2 := Copy(S, FParaX + 1, length(S));
            T := S2;
            if Assigned(FOnBreakLine) then
              FOnBreakLine(Self, S, S2);
            if S2 = T then
            begin
              {$IFDEF MEMOEX_UNDO}
              BeginCompound;
              TInsertUndo.Create(Self, FCaretX, FCaretY, #13#10);
              CaretUndo := false;
              {$ENDIF MEMOEX_UNDO}
              if FAutoIndent then
                F := length(S2) - length(TrimLeft(S2));
              FLines.Insert(FParaY + 1, S2);
              FLines.Internal[FParaY] := Copy(S, 1, FParaX);
              inc(Y);
              { smart tab }
              if (FAutoIndent) and (Trim(FLines.ParaStrings[FCaretY]) <> '')
              { (FLines.ParaStrings[FCaretY][1] = ' ')) or
                ((Trim(FLines.ParaStrings[FCaretY]) = '') and (X > 0)))} then
              begin
                X := GetTabStop(0, Y, tsAutoIndent, true);
                if X > F then
                begin
                  {$IFDEF MEMOEX_UNDO}
                  TInsertUndo.Create(Self, 0, Y, StringOfChar(' ', X - F));
                  {$ENDIF MEMOEX_UNDO}
                  FLines.Internal[FParaY + 1] := StringOfChar(' ', X - F) + S2;
                end;
              end
              else if (FAutoIndent) and (S2 = '') then
                X := length(FLines.ParaStrings[FCaretY])
              else
                X := 0;
              {$IFDEF MEMOEX_UNDO}
              EndCompound;
              {$ENDIF MEMOEX_UNDO}
            end
            else
            begin
              T := Copy(S, 1, FParaX) + #13#10 + S2 + #13#10;
              F := FLines.GetParaOffs(FParaY);
              S2 := FLines.Text;
              System.Delete(S2, F + 1, length(S) + 2);
              System.Insert(T, S2, F + 1);

              FLines.Paragraph2Caret(FParaY, 0, F, _Y);

              {$IFDEF MEMOEX_UNDO}
              CaretUndo := false;
              BeginCompound;
              TCaretUndo.Create(Self, FCaretX, FCaretY);
              TDeleteUndo.Create(Self, 0, _Y, S + #13#10);
              TInsertUndo.Create(Self, 0, _Y, T);
              EndCompound;
              {$ENDIF MEMOEX_UNDO}
              FLines.SetLockText(S2);
              inc(Y);
              X := 0;
            end;
            UpdateEditorSize(false);
            { Invalidate }
            F := RepaintParagraph(FCaretY);
            RedrawFrom(F + 1);
            Changed;
          end;
        ecBackword:
          if not FReadOnly then
          begin
            if length(FLines.ParaStrings[Y]) > 0 then
            begin
              Command(ecBeginCompound);
              Command(ecBeginUpdate);
              Command(ecSelPrevWord);
              Command(ecDeleteSelected);
              Command(ecEndUpdate);
              Command(ecEndCompound);
            end
            else
              Command(ecBackspace);
            Complete;
          end;
        ecBackspace:
          if not FReadOnly then
          begin
            if FSelected then
            begin
              DoAndCorrectXY(DeleteSelected);
              Changed;
            end
            else
            begin
              ReLine;
              FLines.Caret2Paragraph(X, Y, FParaY, FParaX);
              if X > 0 then
              begin
                if FBackSpaceUnindents then
                  X := GetBackStop(FCaretX, FCaretY)
                else
                  X := FCaretX - 1;
                S := Copy(FLines.ParaStrings[FCaretY], X + 1, FCaretX - X);
                dec(FParaX, length(S));
                F := FLines.Paragraphs[FParaY].FCount;
                FLines.InternalParaStrings[Y] := Copy(FLines.ParaStrings[Y], 1, X) + Copy(FLines.ParaStrings[Y], FCaretX + 1, Length(FLines.ParaStrings[Y]));
                FLines.Paragraph2Caret(FParaY, FParaX, X, Y);
                {$IFDEF MEMOEX_UNDO}
                TBackspaceUndo.Create(Self, X, Y, S);
                CaretUndo := false;
                {$ENDIF MEMOEX_UNDO}
                B := F <> FLines.Paragraphs[FParaY].FCount;
                F := RepaintParagraph(Y);
                if B then
                begin
                  UpdateEditorSize(false);
                  RedrawFrom(F + 1);
                end;
                Changed;
              end
              else if Y > 0 then
              begin
                if FParaX > 0 then
                begin
                  T := FLines[FParaY];
                  S := Copy(T, FParaX, 1);

                  System.Delete(T, FParaX, 1);
                  FLines.Internal[FParaY] := T;
                  dec(FParaX);
                  FLines.Paragraph2Caret(FParaY, FParaX, X, Y);

                  {$IFDEF MEMOEX_UNDO}
                  TBackspaceUndo.Create(Self, X, Y, S);
                  CaretUndo := false;
                  {$ENDIF MEMOEX_UNDO}
                end
                else if FParaY > 0 then
                begin
                  inc(FUpdateLock);
                  S := FLines[FParaY - 1];
                  S2 := FLines[FParaY];
                  if Assigned(FOnConcatLine) then
                    FOnConcatLine(Self, S, S2);
                    {$IFDEF MEMOEX_UNDO}
                  CaretUndo := false;
                  FLines.Paragraph2Caret(FParaY - 1, 0, F, _Y);
                  BeginCompound;
                  TCaretUndo.Create(Self, X, Y);
                  TDeleteUndo.Create(Self, 0, _Y, FLines[FParaY - 1] + #13#10 + FLines[FParaY] + #13#10);
                  TInsertUndo.Create(Self, 0, _Y, S + S2 + #13#10);
                  EndCompound;
                    {$ENDIF MEMOEX_UNDO}
                  FLines.Internal[FParaY - 1] := S + S2;
                  FLines.Delete(FParaY);
                  dec(FUpdateLock);
                  FLines.Paragraph2Caret(FParaY - 1, length(S), X, Y);
                end
                else
                  Complete;
                UpdateEditorSize(false);
                F := RepaintParagraph(Y);
                RedrawFrom(F + 1);
                Changed;
              end;
            end;
          end;
        ecDelete:
          if not FReadOnly then
          begin
            if FLines.ParaLineCount = 0 then
              FLines.Add('');
            if FSelected then
            begin
              DoAndCorrectXY(DeleteSelected);
              Changed;
            end
            else
            begin
              ReLine;
              FLines.Caret2Paragraph(X, Y, FParaY, FParaX);
              if X < Length(FLines.ParaStrings[Y]) then
              begin
                {$IFDEF MEMOEX_UNDO}
                TDeleteUndo.Create(Self, FCaretX, FCaretY, FLines.ParaStrings[Y][X + 1]);
                CaretUndo := false;
                {$ENDIF MEMOEX_UNDO}
                F := FLines.Paragraphs[FParaY].FCount;

                FLines.InternalParaStrings[Y] := Copy(FLines.ParaStrings[Y], 1, X) + Copy(FLines.ParaStrings[Y], X + 2, Length(FLines.ParaStrings[Y]));
                FLines.Paragraph2Caret(FParaY, FParaX, X, Y);

                B := F <> FLines.Paragraphs[FParaY].FCount;
                F := RepaintParagraph(Y);
                if B then
                begin
                  UpdateEditorSize(false);
                  RedrawFrom(F + 1);
                end;

                Changed;
              end
              else if (Y >= 0) and (Y <= FLines.ParaLineCount - 2) then
              begin
                S := FLines[FParaY];
                if FParaX < length(S) then
                begin
                  {$IFDEF MEMOEX_UNDO}
                  TDeleteUndo.Create(Self, FCaretX, FCaretY, System.Copy(S, FParaX + 1, 1));
                  CaretUndo := false;
                  {$ENDIF MEMOEX_UNDO}
                  System.Delete(S, FParaX + 1, 1);
                  FLines.Internal[FParaY] := S;
                  FLines.Paragraph2Caret(FParaY, FParaX, X, Y);
                end
                else
                begin
                  inc(FUpdateLock);

                  S := FLines[FParaY];
                  S2 := FLines[FParaY + 1];
                  if Assigned(FOnConcatLine) then
                    FOnConcatLine(Self, S, S2);

                  {$IFDEF MEMOEX_UNDO}
                  CaretUndo := false;
                  FLines.Paragraph2Caret(FParaY, 0, F, _Y);

                  BeginCompound;
                  TCaretUndo.Create(Self, X, Y);
                  TDeleteUndo.Create(Self, 0, _Y, FLines[FParaY] + #13#10 + FLines[FParaY + 1] + #13#10);
                  TInsertUndo.Create(Self, 0, _Y, S + S2 + #13#10);
                  EndCompound;
                  {$ENDIF MEMOEX_UNDO}
                  FLines.Internal[FParaY] := S + S2;
                  FLines.Delete(FParaY + 1);

                  dec(FUpdateLock);
                  FLines.Paragraph2Caret(FParaY, length(S), X, Y);
                end;

                UpdateEditorSize(false);
                F := RepaintParagraph(FCaretY);
                RedrawFrom(F + 1);

                Changed;
              end;
            end;
          end;
        ecTab, ecBackTab:
          if not FReadOnly then
          begin
            if FSelected then
            begin
              if ACommand = ecTab then
                PostCommand(ecIndent)
              else
                PostCommand(ecUnindent);
            end
            else
            begin
              ReLine;
              X := GetTabStop(FCaretX, FCaretY, tsTabStop, ACommand = ecTab);
              if (ACommand = ecTab) and FInsertMode then
              begin
                S := FLines.ParaStrings[FCaretY];
                FLines.Caret2Paragraph(FCaretX, FCaretY, FParaY, FParaX);
                S2 := StringOfChar(' ', X - FCaretX);

                {$IFDEF MEMOEX_UNDO}
                TInsertTabUndo.Create(Self, FCaretX, FCaretY, S2);
                CaretUndo := false;
                {$ENDIF MEMOEX_UNDO}
                Insert(S2, S, FCaretX + 1);

                F := FLines.Paragraphs[FParaY].FCount;

                FLines.InternalParaStrings[FCaretY] := S;
                inc(FParaX, X - FCaretX);
                FLines.Paragraph2Caret(FParaY, FParaX, X, Y);

                B := F <> FLines.Paragraphs[FParaY].FCount;
                F := RepaintParagraph(FCaretY);
                if B then
                begin
                  UpdateEditorSize(false);
                  RedrawFrom(F + 1);
                end;

                Changed;
              end;
            end;
          end;
        ecIndent:
          if not FReadOnly and FSelected and (FSelBegY <> FSelEndY) and
             (FSelBegX = 0) and (FSelEndX = 0) then
          begin
            F := FindNotBlankCharPos(FLines.ParaStrings[FCaretY]);
            S2 := StringOfChar(' ', GetDefTabStop(F, true) - FCaretX);
            S := SelText;
            S := StringReplaceAll(S, #13#10, #13#10 + S2);
            Delete(S, Length(S) - Length(S2) + 1, Length(S2));
            SetSelText1(S2 + S)
          end;
        ecUnIndent:
          if not FReadOnly and FSelected and (FSelBegY <> FSelEndY) and
             (FSelBegX = 0) and (FSelEndX = 0) then
          begin
            F := FindNotBlankCharPos(FLines.ParaStrings[FCaretY]);
            S2 := StringOfChar(' ', GetDefTabStop(F, true) - FCaretX);
            S := SelText;
            S := StringReplaceAll(S, #13#10 + S2, #13#10);
            for iBeg := 1 to Length(S2) do
              if S[1] = ' ' then
                Delete(S, 1, 1)
              else
                Break;
            SetSelText1(S);
          end;
        ecChangeInsertMode:
          begin
            FInsertMode := not FInsertMode;
            StatusChanged;
          end;
        ecClipBoardCut:
          if not FReadOnly then
            DoAndCorrectXY(ClipBoardCut);
        {$ENDIF MEMOEX_EDITOR}
        ecClipBoardCopy:
          ClipBoardCopy;
        {$IFDEF MEMOEX_EDITOR}
        ecClipBoardPaste:
          if not FReadOnly then
            DoAndCorrectXY(ClipBoardPaste);
        ecDeleteSelected:
          if not FReadOnly and FSelected then
            DoAndCorrectXY(DeleteSelected);
        ecDeleteWord:
          if not FReadOnly then
          begin
            if length(FLines.ParaStrings[Y]) = 0 then
              Command(ecDelete)
            else
            begin
              Command(ecBeginCompound);
              Command(ecBeginUpdate);
              Command(ecSelNextWord);
              Command(ecDeleteSelected);
              Command(ecEndUpdate);
              Command(ecEndCompound);
              Complete;
            end;
          end;
        ecDeleteLine:
          if (not FReadOnly) and (Y >= 0) and (Y <= FLines.ParaLineCount - 1) then
          begin
            FLines.Index2ParaIndex(Y, F, _Y);
            B := (not FWordWrap) or (_Y = FLines.Paragraphs[F].FCount - 1);
            Command(ecBeginCompound);
            Command(ecBeginUpdate);
            Command(ecBeginLine);
            Command(ecSelEndLine);
            Command(ecDeleteSelected);
            if B then
              Command(ecDelete);
            Command(ecEndUpdate);
            Command(ecEndCompound);
            Complete;
          end;
        ecSelAll:
          begin
            Command(ecBeginCompound);
            Command(ecBeginUpdate);
            Command(ecBeginDoc);
            Command(ecSelEndDoc);
            Command(ecEndUpdate);
            Command(ecEndCompound);
            Complete;
          end;
        ecToUpperCase:
          if (not FReadOnly) and (FSelected) then
            SelText := DoChangeCase(SelText, RA_CASE_CONVERT_UPPER);
        ecToLowerCase:
          if (not FReadOnly) and (FSelected) then
            SelText := DoChangeCase(SelText, RA_CASE_CONVERT_LOWER);
        ecChangeCase:
          if (not FReadOnly) and (FSelected) then
            SelText := DoChangeCase(SelText, RA_CASE_CONVERT_INVERT);
        {$ENDIF MEMOEX_EDITOR}
        {$IFDEF MEMOEX_UNDO}
        ecUndo:
          if not FReadOnly then
          begin
            FUndoBuffer.Undo;
            PaintCaret(true);
            Complete;
          end;
        ecRedo:
          if not FReadOnly then
          begin
            FUndoBuffer.Redo;
            PaintCaret(true);
            Complete;
          end;
        ecBeginCompound:
          BeginCompound;
        ecEndCompound:
          EndCompound;
        {$ENDIF MEMOEX_UNDO}
        ecSetBookmark0..ecSetBookmark9:
          ChangeBookMark(ACommand - ecSetBookmark0, true);
        ecGotoBookmark0..ecGotoBookmark9:
          begin
            ChangeBookMark(ACommand - ecGotoBookmark0, false);
            X := FCaretX;
            Y := FCaretY;
          end;
        ecInsertMacro0..ecInsertMacroZ:
          if (Assigned(FOnInsertMacro)) and (not FReadOnly) then
          begin
            S := FOnInsertMacro(Self, ACommand - ecInsertMacro0);
            if S = '' then
              exit;
            InsertTextAtCurrentPos(S);
            PaintCaret(true);
            Complete;
          end;
        ecBlockOpA..ecBlockOpZ:
          if (not FReadOnly) and (Assigned(FOnBlockOperation)) and (FSelected) then
            SelText := FOnBlockOperation(Self, ACommand - ecBlockOpA, SelText);
        {$IFDEF MEMOEX_COMPLETION}
        ecCompletionIdentifers:
          if not FReadOnly then
          begin
            FCompletion.DoCompletion(cmIdentifers);
            PaintCaret(true);
            Complete;
          end;
        ecCompletionTemplates:
          if not FReadOnly then
          begin
            FCompletion.DoCompletion(cmTemplates);
            PaintCaret(true);
            Complete;
          end;
        {$ENDIF MEMOEX_COMPLETION}
        ecBeginUpdate:
          BeginUpdate;
        ecEndUpdate:
          EndUpdate;

        ecRecordMacro:
          if FRecording then
            EndRecord(FDefMacro)
          else
            BeginRecord;
        ecPlayMacro:
          begin
            PlayMacro(FDefMacro);
            Complete;
          end;
        ecSaveBlock:
          if (FSelected) and (Assigned(FOnSaveBlock)) then
            FOnSaveBlock(Self, SelText);
        ecInsertBlock:
          if (not FReadOnly) and (Assigned(FOnInsertBlock)) then
          begin
            if FOnInsertBlock(Self, S) then
              InsertTextAtCurrentPos(S);
            PaintCaret(true);
            Complete;
          end;
      end;

      {$IFDEF MEMOEX_UNDO}
      if CaretUndo then
        SetCaret(X, Y)
      else
        SetCaretInternal(X, Y);
      {$ELSE}
      SetCaret(X, Y);
      {$ENDIF MEMOEX_UNDO}
    except
      on E: EComplete do { OK }
        ;
    end;
  finally
    // dec(FUpdateLock);
    PaintCaret(true);
  end;
end;
{$ENDIF}

procedure TCustomMemoEx.PostCommand(ACommand: TEditCommand);
begin
  PostMessage(Handle, WM_EDITCOMMAND, ACommand, 0);
end; { PostCommand }

procedure TCustomMemoEx.ClipboardChanged;
begin
  if (csLoading in ComponentState) or (csDestroying in ComponentState) then
    exit;
  if Assigned(FOnChangeClipboardState) then
    FOnChangeClipboardState(Self, IsClipboardFormatAvailable(CF_TEXT) or
      IsClipboardFormatAvailable({$ifdef UNICODE}CF_UNICODETEXT{$else}CF_OEMTEXT{$endif}));
end;

procedure TCustomMemoEx.WndProc(var Message: TMessage);
var
  Form: TCustomForm;
  pt, temp: TPoint;
begin
  case Message.Msg of
    CM_COLORCHANGED:
      begin
        Message.Result := 0;
        Invalidate;
        exit;
      end;
    WM_MOUSEWHEEL:
      begin
        MouseWheelHandler(Message);
        Message.Result := 0;
        exit;
      end;
    WM_SYSCHAR:
      if Message.wParam = VK_BACK then
      begin
        Message.Result := 0;
        exit;
      end;
    WM_ERASEBKGND:
      begin
      {$IFDEF MEMOEX_NOOPTIMIZE}
        inherited WndProc(Message);
        Message.Result := 1;
      {$ELSE}
        Message.Result := 0;
      {$ENDIF}
        exit;
      end;
    WM_SETFOCUS:
      begin
        Form := GetParentForm(Self);
        if (Form <> nil) and (not Form.SetFocusedControl(Self)) then
          exit;
        CreateCaret(Handle, 0, 2, CellRect.Height - 2);
        PaintCaret(true);
        DoEnter;
      end;
    WM_KILLFOCUS:
      begin
        if csFocusing in ControlState then
          exit;
      {$IFDEF MEMOEX_COMPLETION}
        if FCompletion.FVisible then
          FCompletion.CloseUp(false);
      {$ENDIF MEMOEX_COMPLETION}
        DestroyCaret;
        DoExit;
      end;
    WM_GETDLGCODE:
      begin
        inherited WndProc(Message);
        TWMGetDlgCode(Message).Result := DLGC_WANTARROWS or DLGC_WANTCHARS;
        if FWantTabs then
          TWMGetDlgCode(Message).Result := TWMGetDlgCode(Message).Result or DLGC_WANTTAB;
        exit;
      end;
    WM_HSCROLL:
      begin
        scbHorz.DoScroll(TWMHScroll(Message));
        exit;
      end;
    WM_VSCROLL:
      begin
        scbVert.DoScroll(TWMVScroll(Message));
        exit;
      end;
    WM_SETTINGCHANGE:
      begin
        scbVertWidth := GetSystemMetrics(SM_CXVSCROLL);
        scbHorzHeight := GetSystemMetrics(SM_CYHSCROLL);
      end;
    WM_EDITCOMMAND:
      begin
        Command(Message.WParam);
        Message.Result := ord(true);
        exit;
      end;
    WM_CHANGECBCHAIN:
      begin
        Message.Result := 0;
        if TWMChangeCBChain(Message).Remove = NextClipViewer then
          NextClipViewer := TWMChangeCBChain(Message).Next
        else
          SendMessage(NextClipViewer, WM_CHANGECBCHAIN, TWMChangeCBChain(Message).Remove,
            TWMChangeCBChain(Message).Next);
        exit;
      end;
    WM_DRAWCLIPBOARD:
      begin
        ClipboardChanged;
        SendMessage(NextClipViewer, WM_DRAWCLIPBOARD, 0, 0);
        exit;
      end;
    WM_DESTROY:
      ChangeClipboardChain(Handle, NextClipViewer);
    WM_CONTEXTMENU:
      begin
        pt := SmallPointToPoint(TWMContextMenu(Message).Pos);
        if pt.X < 0 then
          temp := pt
        else
          temp := ScreenToClient(pt);
        if PtInRect(ClientRect, temp) then
          GetWordUnderCursor(temp.X, temp.Y);
      end;
    WM_COPY:
      begin
        PostCommand(ecClipboardCopy);
        Message.Result := ord(true);
        exit;
      end;
  {$IFDEF MEMOEX_EDITOR}
    WM_CUT:
      begin
        if not FReadOnly then
          PostCommand(ecClipboardCut);
        Message.Result := ord(true);
        exit;
      end;
    WM_PASTE:
      begin
        if not FReadOnly then
          PostCommand(ecClipBoardPaste);
        Message.Result := ord(true);
        exit;
      end;
  {$ENDIF}
  end;
  inherited WndProc(Message);
end;

{$IFDEF MEMOEX_EDITOR}

procedure TCustomMemoEx.SetXY(X, Y: integer);
var
  X1, Y1: integer;
begin
  X1 := FLeftCol;
  Y1 := FTopRow;
  if (Y < FTopRow) or (Y > FLastVisibleRow) then
    Y1 := Y - (FVisibleRowCount div 2);
  if (X < FLeftCol) or (X > FVisibleColCount) then
    X1 := X - (FVisibleColCount div 2);
  SetLeftTop(X1, Y1);
  SetCaret(X, Y);
end;

procedure TCustomMemoEx.ChangeBookMark(const BookMark: TBookMarkNum; const Valid: boolean);
begin
  if Valid then
    with FBookMarks[Bookmark] do
      if Valid and (Y = FCaretY) then
        Valid := false
      else
      begin
        X := FCaretX;
        Y := FCaretY;
        Valid := true;
      end
  else
    with FBookMarks[Bookmark] do
      if Valid then
        SetXY(X, Y);
  BookmarkChanged(BookMark);
end;
{$ENDIF}

procedure TCustomMemoEx.BookmarkChanged(BookMark: integer);
begin
  FGutter.Invalidate;
end;

procedure TCustomMemoEx.SelectionChanged;
begin
  if not (csLoading in ComponentState) then
    if Assigned(FOnSelectionChange) then
      FOnSelectionChange(Self);
end;

procedure TCustomMemoEx.SetSel(const ASelX, ASelY: integer);

  procedure UpdateSelected;
  var
    iR: integer;
  begin
    if FUpdateLock = 0 then
    begin
      if (FUpdateSelBegY <> FSelBegY) or (FUpdateSelBegX <> FSelBegX) then
        for iR := Min(FUpdateSelBegY, FSelBegY) to Max(FUpdateSelBegY, FSelBegY) do
          PaintLine(iR, -1, -1);
      if (FUpdateSelEndY <> FSelEndY) or (FUpdateSelEndX <> FSelEndX) then
        for iR := Min(FUpdateSelEndY, FSelEndY) to Max(FUpdateSelEndY, FSelEndY) do
          PaintLine(iR, -1, -1);
      SelectionChanged;
    end;
  end;

var
  SelX, SelY: integer;
begin
  if ASelX < 0 then
    SelX := 0
  else
    SelX := ASelX;
  if ASelY < 0 then
    SelY := 0
  else
    SelY := ASelY;
  if not FSelected then
  begin
    FSelStartX := SelX;
    FSelStartY := SelY;
    FSelEndX := SelX;
    FSelEndY := SelY;
    FSelBegX := SelX;
    FSelBegY := SelY;
    FSelected := true;
  end
  else
  begin
    {$IFDEF MEMOEX_UNDO}
    TSelectUndo.Create(Self, FCaretX, FCaretY, FSelBlock, FSelBegX, FSelBegY, FSelEndX, FSelEndY);
    {$ENDIF MEMOEX_UNDO}
    FUpdateSelBegX := FSelBegX;
    FUpdateSelBegY := FSelBegY;
    FUpdateSelEndX := FSelEndX;
    FUpdateSelEndY := FSelEndY;
    if SelY <= FSelStartY then
      FSelBegY := SelY;
    if SelY >= FSelStartY then
      FSelEndY := SelY;
    if (SelY < FSelStartY) or ((SelY = FSelStartY) and (SelX <= FSelStartX)) then
    begin
      FSelBegX := SelX;
      FSelEndX := FSelStartX;
      FSelEndY := FSelStartY;
    end
    else if (SelY > FSelStartY) or ((SelY = FSelStartY) and (SelX >= FSelStartX)) then
    begin
      FSelBegX := FSelStartX;
      FSelBegY := FSelStartY;
      FSelEndX := SelX;
    end;
    if FSelBegY < 0 then
      FSelBegY := 0;
    FSelected := true;
    if FCompound = 0 then
      UpdateSelected;
  end;
  if FUpdateSelBegY > FSelBegY then
    FUpdateSelBegY := FSelBegY;
  if FUpdateSelEndY < FSelEndY then
    FUpdateSelEndY := FSelEndY;
end;

procedure TCustomMemoEx.Mouse2Cell(const X, Y: integer; var CX, CY: integer);
begin
  CX := Round((X - EditorClient.Left) / FCellRect.Width);
  CY := (Y - EditorClient.Top) div FCellRect.Height;
end;

procedure TCustomMemoEx.Mouse2Caret(const X, Y: integer; var CX, CY: integer);
begin
  Mouse2Cell(X, Y, CX, CY);
  if CX < 0 then
    CX := 0;
  if CY < 0 then
    CY := 0;
  CX := CX + FLeftCol;
  CY := CY + FTopRow;
  if CX > FLastVisibleCol then
    CX := FLastVisibleCol;
  if CY > FLines.ParaLineCount - 1 then
    CY := FLines.ParaLineCount - 1;
end;

procedure TCustomMemoEx.CaretCoord(const X, Y: integer; var CX, CY: integer);
begin
  CX := X - FLeftCol;
  CY := Y - FTopRow;
  if CX < 0 then
    CX := 0;
  if CY < 0 then
    CY := 0;
  CX := FCellRect.Width * CX;
  CY := FCellRect.Height * CY;
end;

function TCustomMemoEx.ExtractStringWithStyle(XX, YY: integer; const From: string;
  Style: word; const LineAttrs: TLineAttrs; out start: integer): string;
var
  i: integer;
  last: integer;
begin
  if Style <> RA_EX_STYLE_DEFAULT then
  begin
    start := XX;
    last := XX;
    if XX <= length(From) then
      for i := XX downto 0 do
        if LineAttrs[i].ex_style = Style then
          start := i
        else
          break;
    for i := XX + 1 to length(From) - 1 do
      if LineAttrs[i].ex_style = Style then
        last := i
      else
        break;
    result := copy(From, start + 1, last - start + 1);
  end
  else
    start := XX;
end;

{ strip invisible }
function TCustomMemoEx.GetAttrDelta(StartFrom, EndTo: integer; const LineAttrs: TLineAttrs): integer;
var
  i, j: integer;
begin
  Result := 0;
  if (readonly) and (FStripInvisible) then
  begin
    j := EndTo;
    i := StartFrom;
    while (i <= j) and (i < SelAttrs_Size) do
    begin
      if LineAttrs[i].FC = LineAttrs[i].BC then
      begin
        inc(Result);
        inc(j);
      end;
      inc(i);
    end;
  end;
end;

function TCustomMemoEx.DoMouseWheel(Shift: TShiftState; WheelDelta: integer; MousePos: TPoint): boolean;
begin
  MouseWheelScroll(WheelDelta);
  Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
end;

procedure TCustomMemoEx.MouseWheelScroll(Delta: integer);
var
  i: integer;
begin
  i := Mouse.WheelScrollLines;
  if Delta > 0 then
    i := -i;
  scbVert.Position := scbVert.Position + i;
  Scroll(true, scbVert.Position);
end;

procedure TCustomMemoEx.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
var
  XX, YY: integer;
begin
  if double_clicked then
  begin
    double_clicked := false;
    exit;
  end;
  {$IFDEF MEMOEX_COMPLETION}
  if FCompletion.FVisible then
    FCompletion.CloseUp(false);
  {$ENDIF MEMOEX_COMPLETION}
  if Button <> mbRight then
  begin // right click = popup menu -> no caret change
    mouse_down := true;
    mouse_dragged := false;
    Mouse2Caret(X, Y, XX, YY);
    PaintCaret(false);
    if (Button = mbLeft) and (not (ssShift in Shift)) then
      SetUnSelected;
    SetFocus;
    if YinBounds(YY) then
    begin
      if not FCursorBeyondEOL then
        if XX > length(FLines.ParaStrings[YY]) then
          XX := length(FLines.ParaStrings[YY]);
      if (ssShift in Shift) and (SelLength = 0) then
        SetSel(FCaretX, FCaretY);
      SetCaret(XX, YY);
      if ssShift in Shift then
        SetSel(XX, YY);
    end;
    PaintCaret(true);
  end;
  inherited MouseDown(Button, Shift, X, Y);
end;

procedure TCustomMemoEx.DblClick;
var
  i, PY, PX, iBeg, iEnd: integer;
  FAttrs: TLineAttrs;
  s: string;
begin
  double_clicked := true;
  if Assigned(FOnDblClick) then
    FOnDblClick(Self);
  if FDoubleClickLine then
  begin
    PaintCaret(false);
    SetSel(0, FCaretY);
    if FCaretY = FLines.ParaLineCount - 1 then
    begin
      SetSel(Length(FLines.ParaStrings[FCaretY]), FCaretY);
      SetCaret(Length(FLines.ParaStrings[FCaretY]), FCaretY);
    end
    else
    begin
      SetSel(0, FCaretY + 1);
      SetCaret(0, FCaretY + 1);
    end;
    PaintCaret(true);
  end
  else if YinBounds(FCaretY) then
  begin
    s := FLines.GetParagraphByIndex(FCaretY, PY, PX);
    GetLineAttr(PY, FCaretY, PX, length(s), 0, length(s), s, FAttrs);
    i := GetAttrDelta(PX, FCaretX + PX, FAttrs);
    if GetWordOnPosEx(FLines.ParaStrings[FCaretY] + ' ', FCaretX + 1 + i, iBeg, iEnd) <> '' then
    begin
      PaintCaret(false);
      SetSel(iBeg - 1, FCaretY);
      SetSel(iEnd - 1, FCaretY);
      SetCaret(iEnd - 1 - i, FCaretY);
      PaintCaret(true);
    end;
  end;
end;

procedure TCustomMemoEx.GetWordUnderCursor(X, Y: integer; aShift: TShiftState = []);
var
  XX, YY, PX, PY, i: integer;
  s: string;
  FAttrs: TLineAttrs;
  start, delta: integer;
begin
  Mouse2Caret(X, Y, XX, YY);
  if YinBounds(YY) then
  begin
    s := FLines.GetParagraphByIndex(YY, PY, PX);
    GetLineAttr(PY, YY, PX, length(s), 0, length(s), s, FAttrs);
    delta := GetAttrDelta(PX, XX + PX, FAttrs);
    i := XX + PX + delta;
    with FWordUnderCursor do
    begin
      if (i > 0) and (i < SelAttrs_Size) then
      begin
        Style := FAttrs[i - 1].ex_style;
        Text := ExtractStringWithStyle(i, YY, s, Style, FAttrs, start);
        TextStart := FLines.GetParaOffs(PY) + start + 1 - delta;
      end
      else
      begin
        Text := ''; // mark no word found
        TextStart := 0;
      end;
      CaretX := XX;
      CaretY := YY;
      ParaIndex := PY;
      ParaOffset := PX;
      Shift := aShift;
    end;
  end;
end;

procedure TCustomMemoEx.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
begin
  timerScroll.Enabled := false;
  mouse_down := false;
  if (Button = mbLeft) and (not mouse_dragged) then
    if Assigned(FOnWordClick) then
    begin
      GetWordUnderCursor(X, Y, Shift);
      if FWordUnderCursor.Text <> '' then
        FOnWordClick(Self, FWordUnderCursor);
    end;
  inherited MouseUp(Button, Shift, X, Y)
end;

procedure TCustomMemoEx.MouseMove(Shift: TShiftState; X, Y: integer);
var
  C: TCursor;
  S: string;
  i, PY, PX: integer;
  FAttrs: TLineAttrs;
begin
  MouseMoveY := Y;
  Mouse2Caret(X, Y, MouseMoveXX, MouseMoveYY);
  if X < FGutterWidth then
    Cursor := crArrow;
  if (Shift = [ssLeft]) and (mouse_down) then
  begin
    mouse_dragged := true;
    Cursor := crIBeam;
    PaintCaret(false);
    if MouseMoveYY <= FLastVisibleRow then
    begin
      if not FCursorBeyondEOL then
        if YinBounds(MouseMoveYY) then begin
          if MouseMoveXX > length(FLines.ParaStrings[MouseMoveYY]) then
            MouseMoveXX := length(FLines.ParaStrings[MouseMoveYY]);
        end
        else
          MouseMoveXX := 0;
      SetSel(MouseMoveXX, MouseMoveYY);
      SetCaret(MouseMoveXX, MouseMoveYY);
    end;
    timerScroll.Enabled := (Y < 0) or (Y > ClientHeight);
    PaintCaret(true);
  end
  else if (Assigned(FOnMouseOver)) and (YinBounds(MouseMoveYY)) then
  begin
    S := FLines.GetParagraphByIndex(MouseMoveYY, PY, PX);
    GetLineAttr(PY, MouseMoveYY, PX, length(S), 0, MouseMoveXX + 1 + PX, S, FAttrs);
    i := MouseMoveXX + PX + GetAttrDelta(PX, MouseMoveXX + PX, FAttrs) - 1;
    if i < SelAttrs_Size then
    begin
      C := crIBeam;
      FOnMouseOver(Self, FAttrs[i].ex_style, C);
      if C <> Cursor then
        Cursor := C;
    end
    else
      Cursor := crIBeam;
  end;
  inherited MouseMove(Shift, X, Y);
end;

procedure TCustomMemoEx.ScrollTimer(Sender: TObject);
begin
  if (MouseMoveY < 0) or (MouseMoveY > ClientHeight) then
  begin
    if (MouseMoveY < -20) then
      dec(MouseMoveYY, FVisibleRowCount)
    else if (MouseMoveY < 0) then
      dec(MouseMoveYY)
    else if (MouseMoveY > ClientHeight + 20) then
      inc(MouseMoveYY, FVisibleRowCount)
    else if (MouseMoveY > ClientHeight) then
      inc(MouseMoveYY);
    PaintCaret(false);
    SetSel(MouseMoveXX, MouseMoveYY);
    SetCaret(MouseMoveXX, MouseMoveYY);
    PaintCaret(true);
  end;
end;

function TCustomMemoEx.GetRealOffs(DefOffs, Index: integer): integer;
var
  l: integer;
begin
  Result := DefOffs;
  if (Index > -1) and (Index < FLines.FParaLinesCount) then
  begin
    l := length(FLines.ParaStrings[Index]);
    if l > 0 then
      if Result > l then
        Result := l
      else

    else
      Result := 0;
  end;
end;

function TCustomMemoEx.GetSelText: string;
var
  sb, se: integer;
begin
  Result := '';
  if not FSelected then
    exit;
  if not FSelBlock then
  begin
    if (FSelBegY < 0) or (FSelBegY > FLines.ParaLineCount - 1) or
       (FSelEndY < 0) or (FSelEndY > FLines.ParaLineCount - 1) then
    begin
      Err;
      Exit;
    end;
    sb := GetRealOffs(FSelBegX, FSelBegY);
    se := GetRealOffs(FSelEndX, FSelEndY);
    if (se = sb) and (FSelBegY = FSelEndY) then
      exit;
    if FSelBegY <> FSelEndY then
    begin
      sb := PosFromCaret(sb, FSelBegY);
      se := PosFromCaret(se, FSelEndY);
      Result := System.Copy(FLines.Text, sb + 1, se - sb + integer(FInclusive));
    end
    else
    begin // AB faster if without #13
      result := system.Copy(FLines.ParaStrings[FSelBegY], sb + 1, se - sb + integer(FInclusive));
    end;
  end;
end;

function TCustomMemoEx.GetSelLength: integer;
begin
//  Result := Length(GetSelText);
  Result := 0;
  if not FSelected then
    exit;
  if not FSelBlock then
  begin
    if (FSelBegY < 0) or (FSelBegY > FLines.ParaLineCount - 1) or (FSelEndY < 0) or
       (FSelEndY > FLines.ParaLineCount - 1) then
      Exit;
    result := PosFromCaret(FSelEndX, FSelEndY) - PosFromCaret(FSelBegX, FSelBegY) + integer(FInclusive);
  end;
end;

procedure TCustomMemoEx.SetSelText(const AValue: string);
begin
  BeginUpdate;
  try
    BeginCompound;
    DeleteSelected;
    if AValue <> '' then
    begin
      InsertText(AValue);
      FSelectedText := true;
      SelStart := PosFromCaret(FSelBegX, FSelBegY) + 1;
      SelLength := Length(AValue);
    end;
    EndCompound;
  finally
    EndUpdate;
  end;
end;

procedure TCustomMemoEx.ClipBoardCopy;
var
  s: string;
begin
  if not FSelBlock then
  begin
    s := GetSelText;
{$ifdef CLIPBOARDPROTECT} // ClipProtect will trunc clipboard to 2KB
    FClip := s;
    if FClipProtect and (length(s) > 2000) then
      setLength(s, 2000);
{$endif}
    _CopyToClipboard(Handle, s, Font.Charset);
  end;
end;

{$IFDEF MEMOEX_EDITOR}

procedure TCustomMemoEx.ReplaceWord(const NewString: string);
var
  iBeg, iEnd: integer;

  function GetWordOnPos2(S: string; P: integer): string;
  begin
    Result := '';
    if P < 1 then
      exit;
    if {$ifdef UNICODE} (S[P] < #255) and {$endif} (AnsiChar(S[P]) in Separators) and
      ((P < 1) or ({$ifdef UNICODE}(S[P - 1] < #255) and {$endif} (AnsiChar(S[P - 1]) in Separators))) then
      inc(P);
    iBeg := P;
    while iBeg >= 1 do
      if {$ifdef UNICODE} (S[iBeg] < #255) and {$endif} (AnsiChar(S[iBeg]) in Separators) then
        break
      else
        dec(iBeg);
    inc(iBeg);
    iEnd := P;
    while iEnd <= Length(S) do
      if {$ifdef UNICODE} (S[iEnd] < #255) and {$endif} (AnsiChar(S[iEnd]) in Separators) then
        break
      else
        inc(iEnd);
    if iEnd > iBeg then
      Result := Copy(S, iBeg, iEnd - iBeg)
    else
      Result := S[P];
  end;

var
  S, W: string;
  X: integer;
begin
  PaintCaret(false);
  BeginUpdate;
  S := FLines.ParaStrings[FCaretY];
  while FCaretX > Length(S) do
    S := S + ' ';
  W := Trim(GetWordOnPos2(S, FCaretX));
  if W = '' then
  begin
    iBeg := FCaretX + 1;
    iEnd := FCaretX
  end;
  {$IFDEF MEMOEX_UNDO}
  CantUndo;
  // TReplaceUndo.Create(Self, FCaretX - Length(W), FCaretY, iBeg, iEnd, W, NewString);
  {$ENDIF MEMOEX_UNDO}
  Delete(S, iBeg, iEnd - iBeg);
  Insert(NewString, S, iBeg);
  FLines.InternalParaStrings[FCaretY] := S;
  X := iBeg + Length(NewString) - 1;
  PaintLine(FCaretY, -1, -1);
  SetCaretInternal(X, FCaretY);
  Changed;
  EndUpdate;
  PaintCaret(true);
end;

procedure TCustomMemoEx.ReplaceWord2(const NewString: string);
var
  S, S1, W: string;
  P, X, Y: integer;
  iBeg, iEnd: integer;
  NewCaret: integer;
begin
  PaintCaret(false);
  if FCaretX > Length(FLines.ParaStrings[FCaretY]) then
    FLines.InternalParaStrings[FCaretY] := FLines.ParaStrings[FCaretY] + StringOfChar(' ', FCaretX - Length(FLines.ParaStrings[FCaretY]));
  S := FLines.Text;
  P := PosFromCaret(FCaretX, FCaretY);
  W := Trim(GetWordOnPosEx(S, P, iBeg, iEnd));
  if W = '' then
  begin
    iBeg := P + 1;
    iEnd := P
  end;
  S1 := NewString;
  NewCaret := Length(NewString);
  {$IFDEF MEMOEX_UNDO}
  TReplaceUndo.Create(Self, FCaretX, FCaretY, iBeg, iEnd, W, S1);
  {$ENDIF MEMOEX_UNDO}
  Delete(S, iBeg, iEnd - iBeg);
  Insert(S1, S, iBeg);
  FLines.Text := S;
  CaretFromPos(iBeg + NewCaret - 1, X, Y);
  SetCaretInternal(X, Y);
  Changed;
  PaintCaret(true);
end;

procedure TCustomMemoEx.InsertText(const Text: string);
var
  S: string;
  P: integer;
  X, Y: integer;
begin
  if Text <> '' then
  begin
    PaintCaret(false);
    BeginUpdate;
    Reline;
    {$IFDEF MEMOEX_UNDO}
    TInsertUndo.Create(Self, FCaretX, FCaretY, Text);
    {$ENDIF MEMOEX_UNDO}
    P := PosFromCaret(FCaretX, FCaretY);
    if PosEx(#13, text) > 0 then
    begin // insert with #13 -> old slow method
      S := FLines.Text;
      Insert(Text, S, P + 1);
      FLines.SetLockText(S);
    end
    else
    begin // new fast method from AB
      S := FLines.ParaStrings[FCaretY];
      while FCaretX > Length(S) do
        S := S + ' ';
      insert(text, S, FCaretX + 1);
      FLines.InternalParaStrings[FCaretY] := S; // will call reformat paragraph
    end;
    CaretFromPos(P + Length(Text), X, Y);
    SetCaretInternal(X, Y);
    Changed;
    EndUpdate;
    PaintCaret(true);
  end;
end;

procedure TCustomMemoEx.InsertTextAtCurrentPos(const _Text: string);
var
  S: string;
begin
  BeginUpdate;
  S := AdjustLineBreaks(_Text);
  if Assigned(FOnTextInsert) then
    FOnTextInsert(Self, S);
  DeleteSelected;
  InsertText(S);
  EndUpdate;
  scbVert.Position := FTopRow;
end;

function CountChar(P: PChar; Ch: char): integer;
begin
  result := 0;
  if P <> nil then
    while P^ <> #0 do
    begin
      if P^ = Ch then
        inc(result);
      inc(P);
    end;
end;

procedure Replace(S, D: PChar);
begin // faster than PosEx()+Insert()
  repeat
    if S^ = '\' then
    begin
      D[0] := '\';
      D[1] := '\';
      inc(D, 2);
      inc(S);
    end
    else if S^ = #0 then
    begin
      D^ := S^;
      break;
    end
    else
    begin
      D^ := S^;
      inc(D);
      inc(S);
    end;
  until false;
end;

function RtfBackSlash(const Text: string): string;
var
  i: integer;
begin
  i := CountChar(pointer(Text), '\');
  if i = 0 then
    result := Text
  else
  begin
    SetLength(result, length(Text) + i);
    Replace(pointer(Text), pointer(result));
  end;
end;

procedure TCustomMemoEx.ClipBoardPaste;
var
  ClipS: string;
begin
  if readonly then
    exit;
  if Assigned(OnClipboardPaste) and OnClipboardPaste(Self) then
    Exit;
  ClipS := _PasteFromClipboard(Handle, Font.Charset, {$ifdef CLIPBOARDPROTECT}FClip, {$endif} false);
  if ClipPasteRtfBackSlashConvert and not IsClipboardFormatAvailable(CF_MEMOEX) then
    InsertTextAtCurrentPos(RtfBackSlash(ClipS))
  else // not from TMemoEx -> '\' -> '\\'
    InsertTextAtCurrentPos(ClipS);
end;

procedure TCustomMemoEx.ClipBoardCut;
begin
  ClipBoardCopy;
  DeleteSelected;
end;

procedure TCustomMemoEx.DeleteSelected;
var
  S, S1: string;
  iBeg, X, Y: integer;
begin
  if FSelected then
  begin
    S1 := GetSelText;
    FSelectedText := false;
    if S1 = '' then
      exit;
    PaintCaret(false);

    iBeg := PosFromCaret(FSelBegX, FSelBegY);
    {$IFDEF MEMOEX_UNDO}
    TDeleteSelectedUndo.Create(Self, FCaretX, FCaretY, S1, FSelBlock, FSelBegX,
      FSelBegY, FSelEndX, FSelEndY, iBeg);
    {$ENDIF MEMOEX_UNDO}
    if FSelBegY <> FSelEndY then
    begin // delete with #13 -> old slow method
      S := FLines.Text;
      Delete(S, iBeg + 1, length(S1));
      FLines.SetLockText(S);
    end
    else
    begin // new fast method from AB
      s := FLines.ParaStrings[FSelBegY];
      while FSelBegX > Length(S) do
        S := S + ' ';
      Delete(S, FSelBegX + 1, length(S1));
      FLines.InternalParaStrings[FSelBegY] := S; // contient reformat paragraph
    end;
    CaretFromPos(iBeg, X, Y);
    SetCaretInternal(X, Y);
    Changed;
    UpdateEditorSize(false);
    if FUpdateLock = 0 then
      Invalidate;
    PaintCaret(true);
  end;
end;
{$ENDIF MEMOEX_EDITOR}

procedure TCustomMemoEx.SetGutterWidth(AWidth: integer);
begin
  if FGutterWidth <> AWidth then
  begin
    FGutterWidth := AWidth;
    UpdateEditorSize;
    Invalidate;
  end;
end;

procedure TCustomMemoEx.SetGutterColor(AColor: TColor);
begin
  if FGutterColor <> AColor then
  begin
    FGutterColor := AColor;
    FGutter.Invalidate;
  end;
end;

procedure TCustomMemoEx.SetFont(Value: TFont);
begin
  FFont.Assign(Value);
end;

procedure TCustomMemoEx.SetLines(ALines: TEditorStrings);
begin
  if ALines <> nil then
    FLines.Assign(ALines);
  CantUndo;
end;

procedure TCustomMemoEx.TextAllChanged;
begin
  TextAllChangedInternal(true);
end;

procedure TCustomMemoEx.TextAllChangedInternal(const Unselect: Boolean);
begin
  if Unselect then
    FSelectedText := false;
  UpdateEditorSize(false);
  if (Showing) and (FUpdateLock = 0) then
    Invalidate;
end;

procedure TCustomMemoEx.SetCols(ACols: integer);
begin
  if FCols <> ACols then
  begin
    FCols := Max(ACols, 1);
    if FCols > FVisibleColCount then
    begin
      scbHorz.Max := FCols;
      scbHorz.Min := 0;
    end
    else
    begin
      scbHorz.Min := 0;
      scbHorz.Max := 0;
    end;
{    if FVisibleColCount < scbHorz.Max then scbHorz.Page := scbHorz.Max - (scbHorz.Max div Max(1, FVisibleColCount)) * FVisibleColCount
      else scbHorz.Page := 1;}
    scbHorz.LargeChange := FVisibleColCount;
  end;
end;

procedure TCustomMemoEx.SetRows(ARows: integer);
begin
  if FRows <> ARows then
  begin
    FRows := Max(ARows, 1);
    if FRows > FVisibleRowCount then
    begin
      scbVert.Max := FRows - FVisibleRowCount;
      scbVert.Min := 0;
    end
    else
    begin
      scbVert.Min := 0;
      scbVert.Max := 0;
    end;
{    if FVisibleRowCount < scbVert.Max then scbVert.Page := scbVert.Max div Max(1, FVisibleRowCount + FVisibleRowCount div 4)
      else scbVert.Page := 1;}
    scbVert.LargeChange := FVisibleRowCount;
  end;
end;

procedure TCustomMemoEx.SetLeftTop(ALeftCol, ATopRow: integer);
begin
  if ALeftCol < 0 then
    ALeftCol := 0;
  if FLeftCol <> ALeftCol then
  begin
    scbHorz.Position := ALeftCol;
    Scroll(false, ALeftCol);
  end;
  if ATopRow < 0 then
    ATopRow := 0;
  if FTopRow <> ATopRow then
  begin
    scbVert.Position := ATopRow;
    Scroll(true, ATopRow);
  end;
end;

procedure TCustomMemoEx.SetScrollBars(Value: TScrollStyle);
begin
  if FScrollBars <> Value then
  begin
    FScrollBars := Value;
    RecreateWnd;
    UpdateEditorSize;
  end;
end;

procedure TCustomMemoEx.SetRightMarginVisible(Value: boolean);
begin
  if FRightMarginVisible <> Value then
  begin
    FRightMarginVisible := Value;
    Invalidate;
  end;
end;

procedure TCustomMemoEx.SetRightMargin(Value: integer);
begin
  if FRightMargin <> Value then
  begin
    FRightMargin := Value;
    FLines.Reformat;
  end;
end;

procedure TCustomMemoEx.SetRightMarginColor(Value: TColor);
begin
  if FRightMarginColor <> Value then
  begin
    FRightMarginColor := Value;
    Invalidate;
  end;
end;

procedure TCustomMemoEx.Changed;
begin
  FModified := true;
  FPEditBuffer := nil;
  if Assigned(FOnChange) then
    FOnChange(Self);
  StatusChanged;
end;

procedure TCustomMemoEx.StatusChanged;
begin
  if not ((csDestroying in ComponentState) or (csLoading in ComponentState)) then
    if Assigned(FOnChangeStatus) then
      FOnChangeStatus(Self);
end;

procedure TCustomMemoEx.CaretFromPos(Pos: integer; var X, Y: integer);
var
  i, j, k: integer;
begin
  k := 0;
  X := -1;
  Y := -1;
  for i := 0 to FLines.Count - 1 do
    with FLines.FList[i] do
    begin
      for j := 0 to FCount - 1 do
      begin
        inc(Y);
        inc(k, length(FStrings[j]));
        if k >= Pos then
        begin
          X := Pos - (k - length(FStrings[j]));
          exit;
        end;
      end;
      inc(k, 2);
    end;
  Y := FLines.ParaLineCount - 1;
  if Y >= 0 then
    X := length(FLines.ParaStrings[Y]);
end;

function TCustomMemoEx.PosFromCaret(X, Y: integer): integer;
var
  i, j, k: integer;
begin
  if Y > FLines.ParaLineCount - 1 then // if after eof -> get max len
    Result := GetTextLen
  else if Y < 0 then // before bof -> -1
    Result := -1
  else
  begin
    Result := 0; // get position
    k := 0;
    for i := 0 to FLines.FCount - 1 do
      with FLines.FList[i] do
      begin
        if k + (FCount - 1) < Y then
        begin
          inc(Result, StringDynArrayGetSize(pointer(FStrings), FCount) + 2);
          inc(k, FCount);
        end
        else
        begin
          for j := 0 to FCount - 1 do
            if k + j < Y then
              inc(Result, length(FStrings[j]));
          inc(Result, X);
          break;
        end;
      end;
  end;
end;

function TCustomMemoEx.PosFromMouse(const X, Y: integer): integer;
var
  X1, Y1: integer;
begin
  Mouse2Caret(X, Y, X1, Y1);
  if (X1 < 0) or (Y1 < 0) then
    Result := -1
  else
    Result := PosFromCaret(X1, Y1);
end;

function TCustomMemoEx.GetTextLen: integer;
var
  i: integer;
begin
  Result := FLines.FCount * 2; // #13+#10 for each line
  for i := 0 to FLines.FCount - 1 do
    with FLines.FList[i] do
      inc(Result, StringDynArrayGetSize(pointer(FStrings), FCount));
end;

function TCustomMemoEx.GetSelStart: integer;
begin
  if FSelectedText then
    Result := PosFromCaret(GetRealOffs(FSelBegX, FSelBegY), FSelBegY) + 1
  else
    Result := PosFromCaret(GetRealOffs(FCaretX, FCaretY), FCaretY) + 1;
end;

procedure TCustomMemoEx.SetSelStart(const ASelStart: integer);
begin
  FSelectedText := true;
  CaretFromPos(ASelStart - 1, FSelBegX, FSelBegY);
  SetCaretInternal(FSelBegX, FSelBegY);
  SetSelLength(0);
  MakeRowVisible(FSelBegY);
end;

procedure TCustomMemoEx.MakeRowVisible(ARow: integer);
begin
  if (ARow < FTopRow) or (ARow > FLastVisibleRow) then
  begin
    ARow := FCaretY - Trunc(VisibleRowCount / 2);
    if ARow < 0 then
      ARow := 0;
    SetLeftTop(FLeftCol, ARow);
  end;
end;

procedure TCustomMemoEx.SetSelLength(const ASelLength: integer);
begin
  FSelectedText := ASelLength > 0;
  CaretFromPos(SelStart + ASelLength - 1, FSelEndX, FSelEndY);
  FUpdateSelBegY := FSelBegY;
  FUpdateSelEndY := FSelEndY;
  SetCaretInternal(FSelEndX, FSelEndY);
  Invalidate;
end;

procedure TCustomMemoEx.SetLockText(const Text: string);
begin
  FLines.SetLockText(Text);
end;

procedure TCustomMemoEx.GutterPaint(Canvas: TCanvas; const Rect: TRect);
begin
  if Assigned(FOnPaintGutter) then
    FOnPaintGutter(Self, Canvas, Rect);
end;

procedure TCustomMemoEx.SetMode(index: integer; Value: boolean);
var
  PB: ^boolean;
begin
  case index of
    0:
      PB := @FInsertMode;
  else {1 :}
    PB := @FReadOnly;
  end;
  if PB^ <> Value then
  begin
    PB^ := Value;
    if index = 1 then
      Invalidate;
    StatusChanged;
  end;
end;

function TCustomMemoEx.GetWordOnCaret: string;
begin
  Result := GetWordOnPos(FLines.ParaStrings[CaretY], CaretX + 1);
end;

function TCustomMemoEx.GetTabStop(const X, Y: integer; const What: TTabStop; const Next: Boolean): integer;

  procedure UpdateTabStops;
  var
    S: string;
    j, i: integer;

    function ProcessString: boolean;
    begin
      Result := false;
      if (What = tsTabStop) and (length(S) > 0) then
        FTabPos[length(S) - 1] := true;
      while i <= length(S) do
      begin
        if S[i] = ' ' then
        begin
          FTabPos[i - 1] := true;
          if i >= X then
            Result := true;
        end;
        inc(i);
      end;
    end;

  begin
    FillChar(FTabPos[0], Max_X, false);
    if (FSmartTab) and (What = tsTabStop) then
    begin
      j := 1;
      i := 1;
      while Y - j >= 0 do
      begin
        S := TrimRight(FLines.ParaStrings[Y - j]);
        if ProcessString then
          break;
        if i >= Max_X div 4 then
          Break;
        if j >= FVisibleRowCount * 2 then
          Break;
        inc(j);
      end;
    end
    else if (What = tsAutoIndent) and FAutoIndent then
    begin
      FLines.Index2ParaIndex(Y, i, j);
      if i - 1 >= 0 then
      begin
        S := FLines[i - 1];
        i := 1;
        ProcessString;
      end;
    end;
  end;

var
  i: integer;
begin
  UpdateTabStops;
  Result := X;
  if Next then
  begin
    for i := X + 1 to High(FTabPos) do
      if (not FTabPos[i - 1]) and (What = tsAutoIndent) then
      begin
        Result := i - 1;
        exit;
      end
      else if (not FTabPos[i]) and (i > 0) then
        if FTabPos[i - 1] then
        begin
          Result := i;
          Exit;
        end;
    if Result = X then
      Result := GetDefTabStop(X, true);
  end
  else if Result = X then
    Result := GetDefTabStop(X, false);
end;

function TCustomMemoEx.GetDefTabStop(const X: integer; const Next: Boolean): integer;
var
  S: string;
  i: integer;
begin
  S := Trim(SubStr(FTabStops, 0, ' '));
  try
    i := StrToInt(S);
  except
    i := 8;
  end;
  if i = 0 then
    Result := X
  else if i > X then
    Result := i
  else if X mod i = 0 then
    Result := X + i
  else
    Result := ((X div i) + 1) * i;
end;

function TCustomMemoEx.GetBackStop(const X, Y: integer): integer;

  procedure UpdateBackStops;
  var
    S: string;
    j, i, k: integer;
  begin
    j := 1;
    i := X - 1;
    FillChar(FTabPos[0], Max_X, false);
    FTabPos[0] := true;
    while Y - j >= 0 do
    begin
      S := FLines.ParaStrings[Y - j];
      for k := 1 to Min(Length(S), i) do { Iterate }
        if S[k] <> ' ' then
        begin
          i := k;
          FTabPos[i - 1] := true;
          Break;
        end;
      if i = 1 then
        Break;
      if j >= FVisibleRowCount * 2 then
        Break;
      inc(j);
    end;
  end;

var
  i: integer;
  S: string;
begin
  Result := X - 1;
  S := TrimRight(FLines.ParaStrings[Y]);
  if (Trim(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;

procedure TCustomMemoEx.BeginCompound;
begin
  inc(FCompound);
  TBeginCompoundUndo.Create(Self);
end;

procedure TCustomMemoEx.EndCompound;
begin
  TEndCompoundUndo.Create(Self);
  dec(FCompound);
end;

procedure TCustomMemoEx.BeginRecord;
begin
  FMacro := '';
  FRecording := true;
  StatusChanged;
end;

procedure TCustomMemoEx.EndRecord(var AMacro: TMacro);
begin
  FRecording := false;
  AMacro := FMacro;
  StatusChanged;
end;

procedure TCustomMemoEx.PlayMacro(const AMacro: TMacro);
var
  i: integer;
begin
  BeginUpdate;
  BeginCompound;
  try
    i := 1;
    while i < Length(AMacro) do
    begin
      Command(byte(AMacro[i]) + byte(AMacro[i + 1]) shl 8);
      inc(i, 2);
    end;
  finally
    EndCompound;
    EndUpdate;
  end;
end;

constructor TEditKey.Create(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState);
begin
  Key1 := AKey1;
  Shift1 := AShift1;
  Command := ACommand;
end;

constructor TEditKey.Create2(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState; const AKey2: word; const AShift2: TShiftState);
begin
  Key1 := AKey1;
  Shift1 := AShift1;
  Key2 := AKey2;
  Shift2 := AShift2;
  Command := ACommand;
end;

constructor TKeyboard.Create;
begin
  List := TList.Create;
end;

destructor TKeyboard.Destroy;
begin
  Clear;
  List.Free;
  inherited Destroy;
end;

procedure TKeyboard.Add(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState);
begin
  List.Add(TEditKey.Create(ACommand, AKey1, AShift1));
end;

procedure TKeyboard.Add2(const ACommand: TEditCommand; const AKey1: word; const AShift1: TShiftState; const AKey2: word; const AShift2: TShiftState);
begin
  List.Add(TEditKey.Create2(ACommand, AKey1, AShift1, AKey2, AShift2));
end;

procedure TKeyboard.Clear;
var
  i: integer;
begin
  for i := 0 to List.Count - 1 do
    TObject(List[i]).Free;
  List.Clear;
end;

function TKeyboard.Command(const AKey: word; const AShift: TShiftState): TEditCommand;
var
  i: integer;
begin
  Result := 0;
  for i := 0 to List.Count - 1 do
    with TEditKey(List[i]) do
      if (Key1 = AKey) and (Shift1 = AShift) then
      begin
        if Key2 = 0 then
          Result := Command
        else
          Result := twoKeyCommand;
        Exit;
      end;
end;

function TKeyboard.Command2(const AKey1: word; const AShift1: TShiftState; const AKey2: word; const AShift2: TShiftState): TEditCommand;
var
  i: integer;
begin
  Result := 0;
  for i := 0 to List.Count - 1 do
    with TEditKey(List[i]) do
      if (Key1 = AKey1) and (Shift1 = AShift1) and (Key2 = AKey2) and (Shift2 = AShift2) then
      begin
        Result := Command;
        Exit;
      end;
end;

{$IFDEF MEMOEX_EDITOR}
{$IFDEF MEMOEX_DEFLAYOUT}

procedure TKeyboard.SetDefLayout;
begin
  Clear;

  Add(ecLeft, VK_LEFT, []);
  Add(ecRight, VK_RIGHT, []);
  Add(ecUp, VK_UP, []);
  Add(ecDown, VK_DOWN, []);
  Add(ecSelLeft, VK_LEFT, [ssShift]);
  Add(ecSelRight, VK_RIGHT, [ssShift]);
  Add(ecSelUp, VK_UP, [ssShift]);
  Add(ecSelDown, VK_DOWN, [ssShift]);
  Add(ecBeginLine, VK_HOME, []);
  Add(ecSelBeginLine, VK_HOME, [ssShift]);
  Add(ecBeginDoc, VK_HOME, [ssCtrl]);
  Add(ecSelBeginDoc, VK_HOME, [ssCtrl, ssShift]);
  Add(ecEndLine, VK_END, []);
  Add(ecSelEndLine, VK_END, [ssShift]);
  Add(ecEndDoc, VK_END, [ssCtrl]);
  Add(ecSelEndDoc, VK_END, [ssCtrl, ssShift]);
  Add(ecPrevWord, VK_LEFT, [ssCtrl]);
  Add(ecNextWord, VK_RIGHT, [ssCtrl]);
  Add(ecSelPrevWord, VK_LEFT, [ssCtrl, ssShift]);
  Add(ecSelNextWord, VK_RIGHT, [ssCtrl, ssShift]);
  Add(ecSelAll, ord('A'), [ssCtrl]);

  Add(ecWindowTop, VK_PRIOR, [ssCtrl]);
  Add(ecWindowBottom, VK_NEXT, [ssCtrl]);
  Add(ecPrevPage, VK_PRIOR, []);
  Add(ecNextPage, VK_NEXT, []);
  Add(ecSelPrevPage, VK_PRIOR, [ssShift]);
  Add(ecSelNextPage, VK_NEXT, [ssShift]);
  Add(ecScrollLineUp, VK_UP, [ssCtrl]);
  Add(ecScrollLineDown, VK_DOWN, [ssCtrl]);

  Add(ecChangeInsertMode, VK_INSERT, []);

  Add(ecInsertPara, VK_RETURN, []);
  Add(ecBackspace, VK_BACK, []);
  Add(ecBackspace, VK_BACK, [ssShift]);
  Add(ecBackword, VK_BACK, [ssCtrl]);
  Add(ecDelete, VK_DELETE, []);
  Add(ecTab, VK_TAB, []);
  Add(ecBackTab, VK_TAB, [ssShift]);
  Add(ecDeleteSelected, VK_DELETE, [ssCtrl]);

  Add(ecClipboardCopy, VK_INSERT, [ssCtrl]);
  Add(ecClipboardCut, VK_DELETE, [ssShift]);
  Add(ecClipBoardPaste, VK_INSERT, [ssShift]);
  Add(ecClipboardCopy, ord('C'), [ssCtrl]);
  Add(ecClipboardCut, ord('X'), [ssCtrl]);
  Add(ecClipBoardPaste, ord('V'), [ssCtrl]);

  Add(ecSetBookmark0, ord('0'), [ssCtrl, ssShift]);
  Add(ecSetBookmark1, ord('1'), [ssCtrl, ssShift]);
  Add(ecSetBookmark2, ord('2'), [ssCtrl, ssShift]);
  Add(ecSetBookmark3, ord('3'), [ssCtrl, ssShift]);
  Add(ecSetBookmark4, ord('4'), [ssCtrl, ssShift]);
  Add(ecSetBookmark5, ord('5'), [ssCtrl, ssShift]);
  Add(ecSetBookmark6, ord('6'), [ssCtrl, ssShift]);
  Add(ecSetBookmark7, ord('7'), [ssCtrl, ssShift]);
  Add(ecSetBookmark8, ord('8'), [ssCtrl, ssShift]);
  Add(ecSetBookmark9, ord('9'), [ssCtrl, ssShift]);

  Add(ecGotoBookmark0, ord('0'), [ssCtrl]);
  Add(ecGotoBookmark1, ord('1'), [ssCtrl]);
  Add(ecGotoBookmark2, ord('2'), [ssCtrl]);
  Add(ecGotoBookmark3, ord('3'), [ssCtrl]);
  Add(ecGotoBookmark4, ord('4'), [ssCtrl]);
  Add(ecGotoBookmark5, ord('5'), [ssCtrl]);
  Add(ecGotoBookmark6, ord('6'), [ssCtrl]);
  Add(ecGotoBookmark7, ord('7'), [ssCtrl]);
  Add(ecGotoBookmark8, ord('8'), [ssCtrl]);
  Add(ecGotoBookmark9, ord('9'), [ssCtrl]);

  Add2(ecSetBookmark0, ord('K'), [ssCtrl], ord('0'), []);
  Add2(ecSetBookmark0, ord('K'), [ssCtrl], ord('0'), [ssCtrl]);
  Add2(ecSetBookmark1, ord('K'), [ssCtrl], ord('1'), []);
  Add2(ecSetBookmark1, ord('K'), [ssCtrl], ord('1'), [ssCtrl]);
  Add2(ecSetBookmark2, ord('K'), [ssCtrl], ord('2'), []);
  Add2(ecSetBookmark2, ord('K'), [ssCtrl], ord('2'), [ssCtrl]);
  Add2(ecSetBookmark3, ord('K'), [ssCtrl], ord('3'), []);
  Add2(ecSetBookmark3, ord('K'), [ssCtrl], ord('3'), [ssCtrl]);
  Add2(ecSetBookmark4, ord('K'), [ssCtrl], ord('4'), []);
  Add2(ecSetBookmark4, ord('K'), [ssCtrl], ord('4'), [ssCtrl]);
  Add2(ecSetBookmark5, ord('K'), [ssCtrl], ord('5'), []);
  Add2(ecSetBookmark5, ord('K'), [ssCtrl], ord('5'), [ssCtrl]);
  Add2(ecSetBookmark6, ord('K'), [ssCtrl], ord('6'), []);
  Add2(ecSetBookmark6, ord('K'), [ssCtrl], ord('6'), [ssCtrl]);
  Add2(ecSetBookmark7, ord('K'), [ssCtrl], ord('7'), []);
  Add2(ecSetBookmark7, ord('K'), [ssCtrl], ord('7'), [ssCtrl]);
  Add2(ecSetBookmark8, ord('K'), [ssCtrl], ord('8'), []);
  Add2(ecSetBookmark8, ord('K'), [ssCtrl], ord('8'), [ssCtrl]);
  Add2(ecSetBookmark9, ord('K'), [ssCtrl], ord('9'), []);
  Add2(ecSetBookmark9, ord('K'), [ssCtrl], ord('9'), [ssCtrl]);

  Add2(ecGotoBookmark0, ord('Q'), [ssCtrl], ord('0'), []);
  Add2(ecGotoBookmark0, ord('Q'), [ssCtrl], ord('0'), [ssCtrl]);
  Add2(ecGotoBookmark1, ord('Q'), [ssCtrl], ord('1'), []);
  Add2(ecGotoBookmark1, ord('Q'), [ssCtrl], ord('1'), [ssCtrl]);
  Add2(ecGotoBookmark2, ord('Q'), [ssCtrl], ord('2'), []);
  Add2(ecGotoBookmark2, ord('Q'), [ssCtrl], ord('2'), [ssCtrl]);
  Add2(ecGotoBookmark3, ord('Q'), [ssCtrl], ord('3'), []);
  Add2(ecGotoBookmark3, ord('Q'), [ssCtrl], ord('3'), [ssCtrl]);
  Add2(ecGotoBookmark4, ord('Q'), [ssCtrl], ord('4'), []);
  Add2(ecGotoBookmark4, ord('Q'), [ssCtrl], ord('4'), [ssCtrl]);
  Add2(ecGotoBookmark5, ord('Q'), [ssCtrl], ord('5'), []);
  Add2(ecGotoBookmark5, ord('Q'), [ssCtrl], ord('5'), [ssCtrl]);
  Add2(ecGotoBookmark6, ord('Q'), [ssCtrl], ord('6'), []);
  Add2(ecGotoBookmark6, ord('Q'), [ssCtrl], ord('6'), [ssCtrl]);
  Add2(ecGotoBookmark7, ord('Q'), [ssCtrl], ord('7'), []);
  Add2(ecGotoBookmark7, ord('Q'), [ssCtrl], ord('7'), [ssCtrl]);
  Add2(ecGotoBookmark8, ord('Q'), [ssCtrl], ord('8'), []);
  Add2(ecGotoBookmark8, ord('Q'), [ssCtrl], ord('8'), [ssCtrl]);
  Add2(ecGotoBookmark9, ord('Q'), [ssCtrl], ord('9'), []);
  Add2(ecGotoBookmark9, ord('Q'), [ssCtrl], ord('9'), [ssCtrl]);
{
  Add2(ecInsertMacro0, ord('S'), [ssCtrl], ord('0'), [ssCtrl]);
  Add2(ecInsertMacro0, ord('S'), [ssCtrl], ord('0'), []);
  Add2(ecInsertMacro1, ord('S'), [ssCtrl], ord('1'), [ssCtrl]);
  Add2(ecInsertMacro1, ord('S'), [ssCtrl], ord('1'), []);
  Add2(ecInsertMacro2, ord('S'), [ssCtrl], ord('2'), [ssCtrl]);
  Add2(ecInsertMacro2, ord('S'), [ssCtrl], ord('2'), []);
  Add2(ecInsertMacro3, ord('S'), [ssCtrl], ord('3'), [ssCtrl]);
  Add2(ecInsertMacro3, ord('S'), [ssCtrl], ord('3'), []);
  Add2(ecInsertMacro4, ord('S'), [ssCtrl], ord('4'), [ssCtrl]);
  Add2(ecInsertMacro4, ord('S'), [ssCtrl], ord('4'), []);
  Add2(ecInsertMacro5, ord('S'), [ssCtrl], ord('5'), [ssCtrl]);
  Add2(ecInsertMacro5, ord('S'), [ssCtrl], ord('5'), []);
  Add2(ecInsertMacro6, ord('S'), [ssCtrl], ord('6'), [ssCtrl]);
  Add2(ecInsertMacro6, ord('S'), [ssCtrl], ord('6'), []);
  Add2(ecInsertMacro7, ord('S'), [ssCtrl], ord('7'), [ssCtrl]);
  Add2(ecInsertMacro7, ord('S'), [ssCtrl], ord('7'), []);
  Add2(ecInsertMacro8, ord('S'), [ssCtrl], ord('8'), [ssCtrl]);
  Add2(ecInsertMacro8, ord('S'), [ssCtrl], ord('8'), []);
  Add2(ecInsertMacro9, ord('S'), [ssCtrl], ord('9'), [ssCtrl]);
  Add2(ecInsertMacro9, ord('S'), [ssCtrl], ord('9'), []);
  Add2(ecInsertMacroA, ord('S'), [ssCtrl], ord('A'), [ssCtrl]);
  Add2(ecInsertMacroA, ord('S'), [ssCtrl], ord('A'), []);
  Add2(ecInsertMacroB, ord('S'), [ssCtrl], ord('B'), [ssCtrl]);
  Add2(ecInsertMacroB, ord('S'), [ssCtrl], ord('B'), []);
  Add2(ecInsertMacroC, ord('S'), [ssCtrl], ord('C'), [ssCtrl]);
  Add2(ecInsertMacroC, ord('S'), [ssCtrl], ord('C'), []);
  Add2(ecInsertMacroD, ord('S'), [ssCtrl], ord('D'), [ssCtrl]);
  Add2(ecInsertMacroD, ord('S'), [ssCtrl], ord('D'), []);
  Add2(ecInsertMacroE, ord('S'), [ssCtrl], ord('E'), [ssCtrl]);
  Add2(ecInsertMacroE, ord('S'), [ssCtrl], ord('E'), []);
  Add2(ecInsertMacroF, ord('S'), [ssCtrl], ord('F'), [ssCtrl]);
  Add2(ecInsertMacroF, ord('S'), [ssCtrl], ord('F'), []);
  Add2(ecInsertMacroG, ord('S'), [ssCtrl], ord('G'), [ssCtrl]);
  Add2(ecInsertMacroG, ord('S'), [ssCtrl], ord('G'), []);
  Add2(ecInsertMacroH, ord('S'), [ssCtrl], ord('H'), [ssCtrl]);
  Add2(ecInsertMacroH, ord('S'), [ssCtrl], ord('H'), []);
  Add2(ecInsertMacroI, ord('S'), [ssCtrl], ord('I'), [ssCtrl]);
  Add2(ecInsertMacroI, ord('S'), [ssCtrl], ord('I'), []);
  Add2(ecInsertMacroJ, ord('S'), [ssCtrl], ord('J'), [ssCtrl]);
  Add2(ecInsertMacroJ, ord('S'), [ssCtrl], ord('J'), []);
  Add2(ecInsertMacroK, ord('S'), [ssCtrl], ord('K'), [ssCtrl]);
  Add2(ecInsertMacroK, ord('S'), [ssCtrl], ord('K'), []);
  Add2(ecInsertMacroL, ord('S'), [ssCtrl], ord('L'), [ssCtrl]);
  Add2(ecInsertMacroL, ord('S'), [ssCtrl], ord('L'), []);
  Add2(ecInsertMacroM, ord('S'), [ssCtrl], ord('M'), [ssCtrl]);
  Add2(ecInsertMacroM, ord('S'), [ssCtrl], ord('M'), []);
  Add2(ecInsertMacroN, ord('S'), [ssCtrl], ord('N'), [ssCtrl]);
  Add2(ecInsertMacroN, ord('S'), [ssCtrl], ord('N'), []);
  Add2(ecInsertMacroO, ord('S'), [ssCtrl], ord('O'), [ssCtrl]);
  Add2(ecInsertMacroO, ord('S'), [ssCtrl], ord('O'), []);
  Add2(ecInsertMacroP, ord('S'), [ssCtrl], ord('P'), [ssCtrl]);
  Add2(ecInsertMacroP, ord('S'), [ssCtrl], ord('P'), []);
  Add2(ecInsertMacroQ, ord('S'), [ssCtrl], ord('Q'), [ssCtrl]);
  Add2(ecInsertMacroQ, ord('S'), [ssCtrl], ord('Q'), []);
  Add2(ecInsertMacroR, ord('S'), [ssCtrl], ord('R'), [ssCtrl]);
  Add2(ecInsertMacroR, ord('S'), [ssCtrl], ord('R'), []);
  Add2(ecInsertMacroS, ord('S'), [ssCtrl], ord('S'), [ssCtrl]);
  Add2(ecInsertMacroS, ord('S'), [ssCtrl], ord('S'), []);
  Add2(ecInsertMacroT, ord('S'), [ssCtrl], ord('T'), [ssCtrl]);
  Add2(ecInsertMacroT, ord('S'), [ssCtrl], ord('T'), []);
  Add2(ecInsertMacroU, ord('S'), [ssCtrl], ord('U'), [ssCtrl]);
  Add2(ecInsertMacroU, ord('S'), [ssCtrl], ord('U'), []);
  Add2(ecInsertMacroV, ord('S'), [ssCtrl], ord('V'), [ssCtrl]);
  Add2(ecInsertMacroV, ord('S'), [ssCtrl], ord('V'), []);
  Add2(ecInsertMacroW, ord('S'), [ssCtrl], ord('W'), [ssCtrl]);
  Add2(ecInsertMacroW, ord('S'), [ssCtrl], ord('W'), []);
  Add2(ecInsertMacroX, ord('S'), [ssCtrl], ord('X'), [ssCtrl]);
  Add2(ecInsertMacroX, ord('S'), [ssCtrl], ord('X'), []);
  Add2(ecInsertMacroY, ord('S'), [ssCtrl], ord('Y'), [ssCtrl]);
  Add2(ecInsertMacroY, ord('S'), [ssCtrl], ord('Y'), []);
  Add2(ecInsertMacroZ, ord('S'), [ssCtrl], ord('Z'), [ssCtrl]);
  Add2(ecInsertMacroZ, ord('S'), [ssCtrl], ord('Z'), []);
}
  {$IFDEF MEMOEX_UNDO}
  Add(ecUndo, ord('Z'), [ssCtrl]);
  Add(ecUndo, VK_BACK, [ssAlt]);
  {$ENDIF MEMOEX_UNDO}

  {$IFDEF MEMOEX_COMPLETION}
  Add(ecCompletionIdentifers, VK_SPACE, [ssCtrl]);
  Add(ecCompletionTemplates, ord('J'), [ssCtrl]);
  {$ENDIF MEMOEX_COMPLETION}

  { cursor movement - default and classic }
  Add2(ecBeginDoc, ord('Q'), [ssCtrl], ord('R'), []);
  Add2(ecEndDoc, ord('Q'), [ssCtrl], ord('C'), []);

  Add2(ecBeginLine, ord('Q'), [ssCtrl], ord('S'), []);
  Add2(ecEndLine, ord('Q'), [ssCtrl], ord('D'), []);

  Add2(ecWindowTop, ord('Q'), [ssCtrl], ord('E'), []);
  Add2(ecWindowBottom, ord('Q'), [ssCtrl], ord('X'), []);

  Add2(ecWindowTop, ord('Q'), [ssCtrl], ord('T'), []);
  Add2(ecWindowBottom, ord('Q'), [ssCtrl], ord('U'), []);

  Add(ecDeleteWord, ord('T'), [ssCtrl]);
  Add(ecInsertPara, ord('N'), [ssCtrl]);
  Add(ecDeleteLine, ord('Y'), [ssCtrl]);

  Add2(ecSelWord, ord('K'), [ssCtrl], ord('T'), [ssCtrl]);
  Add2(ecToUpperCase, ord('K'), [ssCtrl], ord('O'), [ssCtrl]);
  Add2(ecToLowerCase, ord('K'), [ssCtrl], ord('N'), [ssCtrl]);
  Add2(ecChangeCase, ord('O'), [ssCtrl], ord('U'), [ssCtrl]);

  Add2(ecInsertBlock, ord('K'), [ssCtrl], ord('R'), [ssCtrl]);
  Add2(ecSaveBlock, ord('K'), [ssCtrl], ord('W'), [ssCtrl]);

  Add(ecRecordMacro, ord('R'), [ssCtrl, ssShift]);
  Add(ecPlayMacro, ord('P'), [ssCtrl]);
{
  Add2(ecBlockOpA, ord('B'), [ssCtrl], ord('A'), [ssCtrl]);
  Add2(ecBlockOpA, ord('B'), [ssCtrl], ord('A'), []);
  Add2(ecBlockOpB, ord('B'), [ssCtrl], ord('B'), [ssCtrl]);
  Add2(ecBlockOpB, ord('B'), [ssCtrl], ord('B'), []);
  Add2(ecBlockOpC, ord('B'), [ssCtrl], ord('C'), [ssCtrl]);
  Add2(ecBlockOpC, ord('B'), [ssCtrl], ord('C'), []);
  Add2(ecBlockOpD, ord('B'), [ssCtrl], ord('D'), [ssCtrl]);
  Add2(ecBlockOpD, ord('B'), [ssCtrl], ord('D'), []);
  Add2(ecBlockOpE, ord('B'), [ssCtrl], ord('E'), [ssCtrl]);
  Add2(ecBlockOpE, ord('B'), [ssCtrl], ord('E'), []);
  Add2(ecBlockOpF, ord('B'), [ssCtrl], ord('F'), [ssCtrl]);
  Add2(ecBlockOpF, ord('B'), [ssCtrl], ord('F'), []);
  Add2(ecBlockOpG, ord('B'), [ssCtrl], ord('G'), [ssCtrl]);
  Add2(ecBlockOpG, ord('B'), [ssCtrl], ord('G'), []);
  Add2(ecBlockOpH, ord('B'), [ssCtrl], ord('H'), [ssCtrl]);
  Add2(ecBlockOpH, ord('B'), [ssCtrl], ord('H'), []);
  Add2(ecBlockOpI, ord('B'), [ssCtrl], ord('I'), [ssCtrl]);
  Add2(ecBlockOpI, ord('B'), [ssCtrl], ord('I'), []);
  Add2(ecBlockOpJ, ord('B'), [ssCtrl], ord('J'), [ssCtrl]);
  Add2(ecBlockOpJ, ord('B'), [ssCtrl], ord('J'), []);
  Add2(ecBlockOpK, ord('B'), [ssCtrl], ord('K'), [ssCtrl]);
  Add2(ecBlockOpK, ord('B'), [ssCtrl], ord('K'), []);
  Add2(ecBlockOpL, ord('B'), [ssCtrl], ord('L'), [ssCtrl]);
  Add2(ecBlockOpL, ord('B'), [ssCtrl], ord('L'), []);
  Add2(ecBlockOpM, ord('B'), [ssCtrl], ord('M'), [ssCtrl]);
  Add2(ecBlockOpM, ord('B'), [ssCtrl], ord('M'), []);
  Add2(ecBlockOpN, ord('B'), [ssCtrl], ord('N'), [ssCtrl]);
  Add2(ecBlockOpN, ord('B'), [ssCtrl], ord('N'), []);
  Add2(ecBlockOpO, ord('B'), [ssCtrl], ord('O'), [ssCtrl]);
  Add2(ecBlockOpO, ord('B'), [ssCtrl], ord('O'), []);
  Add2(ecBlockOpP, ord('B'), [ssCtrl], ord('P'), [ssCtrl]);
  Add2(ecBlockOpP, ord('B'), [ssCtrl], ord('P'), []);
  Add2(ecBlockOpQ, ord('B'), [ssCtrl], ord('Q'), [ssCtrl]);
  Add2(ecBlockOpQ, ord('B'), [ssCtrl], ord('Q'), []);
  Add2(ecBlockOpR, ord('B'), [ssCtrl], ord('R'), [ssCtrl]);
  Add2(ecBlockOpR, ord('B'), [ssCtrl], ord('R'), []);
  Add2(ecBlockOpS, ord('B'), [ssCtrl], ord('S'), [ssCtrl]);
  Add2(ecBlockOpS, ord('B'), [ssCtrl], ord('S'), []);
  Add2(ecBlockOpT, ord('B'), [ssCtrl], ord('T'), [ssCtrl]);
  Add2(ecBlockOpT, ord('B'), [ssCtrl], ord('T'), []);
  Add2(ecBlockOpU, ord('B'), [ssCtrl], ord('U'), [ssCtrl]);
  Add2(ecBlockOpU, ord('B'), [ssCtrl], ord('U'), []);
  Add2(ecBlockOpV, ord('B'), [ssCtrl], ord('V'), [ssCtrl]);
  Add2(ecBlockOpV, ord('B'), [ssCtrl], ord('V'), []);
  Add2(ecBlockOpW, ord('B'), [ssCtrl], ord('W'), [ssCtrl]);
  Add2(ecBlockOpW, ord('B'), [ssCtrl], ord('W'), []);
  Add2(ecBlockOpX, ord('B'), [ssCtrl], ord('X'), [ssCtrl]);
  Add2(ecBlockOpX, ord('B'), [ssCtrl], ord('X'), []);
  Add2(ecBlockOpY, ord('B'), [ssCtrl], ord('Y'), [ssCtrl]);
  Add2(ecBlockOpY, ord('B'), [ssCtrl], ord('Y'), []);
  Add2(ecBlockOpZ, ord('B'), [ssCtrl], ord('Z'), [ssCtrl]);
  Add2(ecBlockOpZ, ord('B'), [ssCtrl], ord('Z'), []);
}
end;
{$ENDIF MEMOEX_DEFLAYOUT}


{$IFDEF MEMOEX_UNDO}

procedure RedoNotImplemented;
begin
  raise EMemoExError.Create('Redo not yet implemented');
end;

procedure TCustomMemoEx.CantUndo;
begin
  FUndoBuffer.Clear;
end;

{  TUndoBuffer }

constructor TUndoBuffer.Create;
begin
  FCancelUndo := false;
  FPtr := -1;
end;

procedure TUndoBuffer.Add(AUndo: TUndo);
begin
  if (self = nil) or InUndo then
    exit;
  if FCancelUndo then
    Clear
  else
    FMemoEx.StatusChanged;
  while (Count > 0) and (FPtr < Count - 1) do
  begin
    TUndo(Items[FPtr + 1]).Free;
    inherited Delete(FPtr + 1);
  end;
  inherited Add(AUndo);
  FPtr := Count - 1;
end;

procedure TUndoBuffer.Undo;
var
  UndoClass: TClass;
  Compound: integer;
begin
  InUndo := true;
  try
    if LastUndo <> nil then
    begin
      Compound := 0;
      UndoClass := LastUndo.ClassType;
      while (LastUndo <> nil) and  ((UndoClass = LastUndo.ClassType) or
        (LastUndo is TDeleteTrailUndo) or (LastUndo is TReLineUndo) or (Compound > 0)) do
      begin
        if LastUndo.ClassType = TBeginCompoundUndo then
        begin
          dec(Compound);
          UndoClass := nil;
        end
        else if LastUndo.ClassType = TEndCompoundUndo then
          inc(Compound);
        LastUndo.Undo;
        dec(FPtr);
        if (UndoClass = TDeleteTrailUndo) or (UndoClass = TReLineUndo) then
          UndoClass := LastUndo.ClassType;
        if not FMemoEx.FGroupUndo then
          break;
        // FMemoEx.Paint; {DEBUG !!!!!!!!!}
      end;
      if FMemoEx.FUpdateLock = 0 then
      begin
        FMemoEx.TextAllChangedInternal(false);
        FMemoEx.Changed;
      end;
    end;
  finally
    InUndo := false;
  end;
end;

procedure TUndoBuffer.Redo;
begin
  inc(FPtr);
  LastUndo.Redo;
end;

procedure TUndoBuffer.Clear;
var
  i: integer;
begin // AB: memory leak correction
  for i := 0 to Count - 1 do
    TUndo(Items[i]).Free;
  inherited;
  FCancelUndo := false;
  FMemoEx.StatusChanged;
end;

procedure TUndoBuffer.Delete;
begin
  if Count > 0 then
  begin
    TUndo(Items[Count - 1]).Free;
    inherited Delete(Count - 1);
  end;
end;

function TUndoBuffer.LastUndo: TUndo;
begin
  if (FPtr >= 0) and (Count > 0) then
    Result := TUndo(Items[FPtr])
  else
    Result := nil;
end;

function TUndoBuffer.IsNewGroup(const AUndo: TUndo): boolean;
begin
  Result := (LastUndo = nil) or (LastUndo.ClassType <> AUndo.ClassType)
end;


{ TUndo }

constructor TUndo.Create(const AMemoEx: TCustomMemoEx);
begin
  FMemoEx := AMemoEx;
  UndoBuffer.Add(Self);
end;

function TUndo.UndoBuffer: TUndoBuffer;
begin
  if FMemoEx <> nil then
    Result := FMemoEx.FUndoBuffer
  else
    Result := nil;
end;


{ TCaretUndo }

constructor TCaretUndo.Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer);
begin
  inherited Create(AMemoEx);
  FCaretX := ACaretX;
  FCaretY := ACaretY;
end;

procedure TCaretUndo.Undo;
begin
  with UndoBuffer do
  begin
    dec(FPtr);
    while FMemoEx.FGroupUndo and (FPtr >= 0) and not IsNewGroup(Self) do
      dec(FPtr);
    inc(FPtr);
    with TCaretUndo(Items[FPtr]) do
      FMemoEx.SetCaretInternal(FCaretX, FCaretY);
  end;
end;

procedure TCaretUndo.Redo;
begin
  RedoNotImplemented;
end;


{ TInsertUndo }

constructor TInsertUndo.Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
  const AText: string);
var
  i: integer;
begin
  inherited Create(AMemoEx, ACaretX, ACaretY);
  FText := AText;
  AMemoEx.FLines.Caret2Paragraph(ACaretX, ACaretY, i, FParaOffset);
  FOffset := AMemoEx.FLines.GetParaOffs(i) + FParaOffset;
end;

procedure TInsertUndo.Undo;
var
  S, Text: string;
  _P, _PI: integer;
begin
  Text := '';
  with UndoBuffer do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := TInsertUndo(LastUndo).FText + Text;
      dec(FPtr);
      if not FMemoEx.FGroupUndo then
        break;
    end;
    inc(FPtr);
  end;
  with TInsertUndo(UndoBuffer.Items[UndoBuffer.FPtr]) do
  begin
    if PosEx(#13, text) > 0 then
    begin
      S := FMemoEx.FLines.Text;
      Delete(S, FOffset + 1, Length(Text));
      FMemoEx.FLines.SetLockText(S);
    end
    else
    begin // new fast method by AB
      FMemoEx.FLines.Index2ParaIndex(FCaretY, _P, _PI);
      s := FMemoEX.FLines[_P];
      delete(s, FParaOffset + 1, length(Text));
      FMemoEx.FLines[_P] := S; // will reformat paragraph
    end;
    FMemoEx.SetCaretInternal(FCaretX, FCaretY);
  end;
end;

{ TOverwriteUndo }

constructor TOverwriteUndo.Create(const AMemoEx: TCustomMemoEx;
  const ACaretX, ACaretY: integer; const AOldText, ANewText: string);
var
  i, j: integer;
begin
  inherited Create(AMemoEx, ACaretX, ACaretY);
  FOldText := AOldText;
  FNewText := ANewText;
  AMemoEx.FLines.Caret2Paragraph(ACaretX, ACaretY, i, j);
  FOffset := AMemoEx.FLines.GetParaOffs(i) + j;
end;

procedure TOverwriteUndo.Undo;
var
  S, OldText, NewText: string;
begin
  NewText := '';
  OldText := '';
  with UndoBuffer do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      OldText := TOverwriteUndo(LastUndo).FOldText + OldText;
      NewText := TOverwriteUndo(LastUndo).FNewText + NewText;
      dec(FPtr);
      if not FMemoEx.FGroupUndo then
        break;
    end;
    inc(FPtr);
  end;
  with TOverwriteUndo(UndoBuffer.Items[UndoBuffer.FPtr]) do
  begin
    S := FMemoEx.FLines.Text;
    Delete(S, FOffset + 1, Length(NewText));
    Insert(OldText, S, FOffset + 1);
    FMemoEx.FLines.SetLockText(S);
    FMemoEx.SetCaretInternal(FCaretX, FCaretY);
  end;
end;

{ TDeleteUndo }

procedure TDeleteUndo.Undo;
var
  X, Y: integer;
  S, Text: string;
  iBeg: integer;
begin
  Text := '';
  X := -1;
  Y := -1;
  with UndoBuffer do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      if (X = -1) or (Y = -1) then
      begin
        X := TDeleteUndo(LastUndo).FCaretX;
        Y := TDeleteUndo(LastUndo).FCaretY;
      end;
      Text := TDeleteUndo(LastUndo).FText + Text;
      dec(FPtr);
      if not FMemoEx.FGroupUndo then
        break;
    end;
    inc(FPtr);
  end;
  if (X <> -1) and (Y <> -1) then
    with TDeleteUndo(UndoBuffer.Items[UndoBuffer.FPtr]) do
    begin
      S := FMemoEx.FLines.Text;
      iBeg := FMemoEx.PosFromCaret(X, Y);
      Insert(Text, S, iBeg + 1);
      FMemoEx.FLines.SetLockText(S);
      FMemoEx.CaretFromPos(iBeg, X, Y);
      FMemoEx.SetCaretInternal(X, Y);
    end;
end;

{ TBackspaceUndo }

procedure TBackspaceUndo.Undo;
var
  S, Text: string;
  iBeg: integer;
  X, Y: integer;
begin
  Text := '';
  X := -1;
  Y := -1;
  with UndoBuffer do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      if (X = -1) or (Y = -1) then
      begin
        X := TDeleteUndo(LastUndo).FCaretX;
        Y := TDeleteUndo(LastUndo).FCaretY;
      end;
      Text := Text + TDeleteUndo(LastUndo).FText;
      dec(FPtr);
      if not FMemoEx.FGroupUndo then
        break;
    end;
    inc(FPtr);
  end;
  if (X <> -1) and (Y <> -1) then
    with TDeleteUndo(UndoBuffer.Items[UndoBuffer.FPtr]) do
    begin
      S := FMemoEx.FLines.Text;
      iBeg := FMemoEx.PosFromCaret(X, Y);
      Insert(Text, S, iBeg + 1);
      FMemoEx.FLines.SetLockText(S);
      FMemoEx.CaretFromPos(iBeg + length(Text), X, Y);
      FMemoEx.SetCaretInternal(X, Y);
    end;
end;

{ TReplaceUndo }

constructor TReplaceUndo.Create(const AMemoEx: TCustomMemoEx; const ACaretX, ACaretY: integer;
  const ABeg, AEnd: integer; const AText, ANewText: string);
begin
  inherited Create(AMemoEx, ACaretX, ACaretY);
  FBeg := ABeg;
  FEnd := AEnd;
  FText := AText;
  FNewText := ANewText;
end;

procedure TReplaceUndo.Undo;
var
  S: string;
begin
  S := FMemoEx.FLines.Text;
  Delete(S, FBeg, Length(FNewText));
  Insert(FText, S, FBeg);
  FMemoEx.FLines.SetLockText(S);
  FMemoEx.SetCaretInternal(FCaretX, FCaretY);
end;

{ TDeleteSelectedUndo }

constructor TDeleteSelectedUndo.Create(const AMemoEx: TCustomMemoEx;
  const ACaretX, ACaretY: integer; const AText: string; const ASelBlock: boolean;
  const ASelBegX, ASelBegY, ASelEndX, ASelEndY, ASelOffs: integer);
begin
  inherited Create(AMemoEx, ACaretX, ACaretY, AText);
  FSelBlock := ASelBlock;
  FSelBegX := ASelBegX;
  FSelBegY := ASelBegY;
  FSelEndX := ASelEndX;
  FSelEndY := ASelEndY;
  FSelOffs := ASelOffs;
end;

procedure TDeleteSelectedUndo.Undo;
var
  S, Text: string;
begin
  Text := '';
  with UndoBuffer do
  begin
    while (FPtr >= 0) and not IsNewGroup(Self) do
    begin
      Text := TDeleteUndo(LastUndo).FText + Text;
      dec(FPtr);
      if not FMemoEx.FGroupUndo then
        break;
    end;
    inc(FPtr);
  end;
  with TDeleteUndo(UndoBuffer.Items[UndoBuffer.FPtr]) do
  begin
    S := FMemoEx.FLines.Text;
    // iBeg := FMemoEx.PosFromCaret(FSelBegX, FSelBegY); BUG if wordwrap
    Insert(Text, S, FSelOffs + 1);
    FMemoEx.FLines.SetLockText(S);
    FMemoEx.FSelBlock := FSelBlock;
    FMemoEx.FSelBegX := FSelBegX;
    FMemoEx.FSelBegY := FSelBegY;
    FMemoEx.FSelEndX := FSelEndX;
    FMemoEx.FSelEndY := FSelEndY;
    FMemoEx.FSelectedText := Length(FText) > 0;
    FMemoEx.SetCaretInternal(FCaretX, FCaretY);
  end;
end;

{ TSelectUndo }

constructor TSelectUndo.Create(const AMemoEx: TCustomMemoEx;
  const ACaretX, ACaretY: integer; const ASelBlock: boolean;
  const ASelBegX, ASelBegY, ASelEndX, ASelEndY: integer);
begin
  inherited Create(AMemoEx, ACaretX, ACaretY);
  FSelBlock := ASelBlock;
  FSelBegX := ASelBegX;
  FSelBegY := ASelBegY;
  FSelEndX := ASelEndX;
  FSelEndY := ASelEndY;
end;

procedure TSelectUndo.Undo;
begin
  FMemoEx.FSelectedText := (FSelBegX <> FSelEndX) or (FSelBegY <> FSelEndY);
  FMemoEx.FSelBegX := FSelBegX;
  FMemoEx.FSelBegY := FSelBegY;
  FMemoEx.FSelEndX := FSelEndX;
  FMemoEx.FSelEndY := FSelEndY;
  FMemoEx.FSelBlock := FSelBlock;
  FMemoEx.SetCaretInternal(FCaretX, FCaretY);
end;

{ TBeginCompoundUndo }

procedure TBeginCompoundUndo.Undo;
begin
  { nothing }
end;

{$ENDIF MEMOEX_UNDO}



{$IFDEF MEMOEX_COMPLETION}

procedure TCustomMemoEx.CompletionIdentifer(var Cancel: boolean);
begin
  {abstract}
end;

procedure TCustomMemoEx.CompletionTemplate(var Cancel: boolean);
begin
  {abstract}
end;

procedure TCustomMemoEx.DoCompletionIdentifer(var Cancel: boolean);
begin
  CompletionIdentifer(Cancel);
  if Assigned(FOnCompletionIdentifer) then
    FOnCompletionIdentifer(Self, Cancel);
end;

procedure TCustomMemoEx.DoCompletionTemplate(var Cancel: boolean);
begin
  CompletionTemplate(Cancel);
  if Assigned(FOnCompletionTemplate) then
    FOnCompletionTemplate(Self, Cancel);
end;

function TCustomMemoEx.DoPreprocessCompletion(const ID, OldText: string): string;
begin
  if Assigned(FOnPreprocessCompletion) then
    Result := FOnPreprocessCompletion(Self, ID, OldText)
  else
    Result := OldText;
end;

type
  TMemoExCompletionList = class(TListBox)
  private
    FTimer: TTimer;
    YY: integer;
    // HintWindow : THintWindow;
    procedure CMHintShow(var Message: TMessage); message CM_HINTSHOW;
    procedure WMCancelMode(var Message: TMessage); message WM_CancelMode;
    procedure OnTimer(Sender: TObject);
  protected
    procedure CreateParams(var Params: TCreateParams); override;
    procedure CreateWnd; override;
    procedure DestroyWnd; override;
    procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
    procedure DrawItem(Index: integer; Rect: TRect; State: TOwnerDrawState); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

constructor TCompletion.Create2(AMemoEx: TCustomMemoEx);
begin
  inherited Create;
  FMemoEx := AMemoEx;
  FPopupList := TMemoExCompletionList.Create(FMemoEx);
  FItemHeight := FPopupList.ItemHeight;
  FDropDownCount := 6;
  FDropDownWidth := 300;
  FTimer := TTimer.Create(nil);
  FTimer.Enabled := false;
  FTimer.Interval := 800;
  FTimer.OnTimer := OnTimer;
  FIdentifers := TStringList.Create;
  FTemplates := TStringList.Create;
  FItems := TStringList.Create;
  FAutoChange := TStringList.Create;
  TStringList(FAutoChange).OnChange := AutoChangeChanged;
  FAutoChangeList := TList.Create;
  FDefMode := cmIdentifers;
  FCaretChar := '|';
  FCRLF := '/n';
  FSeparator := '=';
end;

destructor TCompletion.Destroy;
begin
  inherited Destroy;
  FPopupList.Free;
  FIdentifers.Free;
  FTemplates.Free;
  FItems.Free;
  FAutoChange.Free;
  ClearAutoChangeList;
  FAutoChangeList.Free;
  FTimer.Free;
end;

function TCompletion.GetItems: TStrings;
begin
  case FMode of
    cmIdentifers:
      Result := FIdentifers;
    cmTemplates:
      Result := FTemplates;
  else
    Result := nil;
  end;
end;

procedure TCompletion.ReplaceWord(const ANewString: string);
var
  S, S1, W, NewString: string;
  P, X, Y: integer;
  iBeg, iEnd: integer;
  NewCaret, LNum, CX, CY, i: integer;
begin
  with FMemoEx do
  begin
    PaintCaret(false);
    BeginUpdate;
    ReLine;
    S := FLines.Text;
    P := PosFromCaret(FCaretX, FCaretY);
    W := Trim(GetWordOnPosEx(S, P, iBeg, iEnd));
    LNum := 0;
    if W = '' then
    begin
      iBeg := P + 1;
      iEnd := P
    end;
    CaretFromPos(iBeg, CX, CY);
    if CX < 1 then
      CX := FCaretX + 1;
    NewString := DoPreprocessCompletion(W, ANewString);
    case FMode of
      cmIdentifers:
        begin
          S1 := NewString;
          NewCaret := Length(NewString);
        end;
      cmTemplates:
        begin
          S1 := StringReplaceAll(NewString, FCRLF, #13#10 + StringOfChar(' ', CX - 1));
          S1 := StringReplaceAll(S1, FCaretChar, '');
          NewCaret := Pos(FCaretChar, NewString) - 1;
          if NewCaret = -1 then
            NewCaret := Length(NewString);
          for i := 1 to NewCaret do
            if S1[i] = #13 then
              inc(LNum);
        end
    else
      raise EMemoExError.Create('Invalid MemoEx Completion Mode');
    end;
    {$IFDEF MEMOEX_UNDO}
    TReplaceUndo.Create(FMemoEx, FCaretX, FCaretY, iBeg, iEnd, W, S1);
    {$ENDIF MEMOEX_UNDO}
{    //  LW := Length(W);
    if FSelected then
    begin
      if (FSelBegY <= FCaretY) or (FCaretY >= FSelEndY) then
    end;}
    Delete(S, iBeg, iEnd - iBeg);
    Insert(S1, S, iBeg);
    FLines.SetLockText(S);
    CaretFromPos(iBeg - 1 + (CX - 1) * LNum + NewCaret, X, Y);
    SetCaretInternal(X, Y);
    FMemoEx.TextAllChanged; // Invalidate; {!!!}
    Changed;
    EndUpdate;
    PaintCaret(true);
  end;
end;

procedure TCompletion.DoKeyPress(Key: Char);
begin
  if FVisible then
    if {$ifdef UNICODE} (Key < #127) and {$endif}(AnsiChar(Key) in RAEditorCompletionChars) then
      SelectItem
    else
      CloseUp(true)
  else if FEnabled then
    FTimer.Enabled := true;
end;

function TCompletion.DoKeyDown(Key: Word; Shift: TShiftState): boolean;
begin
  Result := true;
  case Key of
    VK_ESCAPE:
      CloseUp(false);
    VK_RETURN:
      CloseUp(true);
    VK_UP, VK_DOWN, VK_PRIOR, VK_NEXT:
      FPopupList.Perform(WM_KEYDOWN, Key, 0);
  else
    Result := false;
  end;
end;

procedure TCompletion.DoCompletion(const AMode: TCompletionList);
var
  Eq: boolean;
  Cancel: boolean;
begin
  if FMemoEx.FReadOnly then
    exit;
  if FPopupList.Visible then
    CloseUp(false);
  FMode := AMode;
  case FMode of
    cmIdentifers:
      DropDown(AMode, true);
    cmTemplates:
      begin
        Cancel := false;
        // FMemoEx.DoCompletionIdentifer(Cancel);
        FMemoEx.DoCompletionTemplate(Cancel);
        if Cancel or (FTemplates.Count = 0) then
          exit;
        MakeItems;
        FindSelItem(Eq);
        if Eq then
          ReplaceWord(SubStr(FItems[ItemIndex], 2, FSeparator))
        else
          DropDown(AMode, true);
      end;
  end;
end;

procedure TCompletion.DropDown(const AMode: TCompletionList; const ShowAlways: boolean);
var
  ItemCount: integer;
  P: TPoint;
  Y: integer;
  PopupWidth, PopupHeight: integer;
  SysBorderWidth, SysBorderHeight: integer;
  R: TRect;
  Cancel: boolean;
  Eq: boolean;
begin
  CloseUp(false);
  FMode := AMode;
  with FMemoEx do
  begin
    Cancel := false;
    case FMode of
      cmIdentifers:
        FMemoEx.DoCompletionIdentifer(Cancel);
      cmTemplates:
        FMemoEx.DoCompletionTemplate(Cancel)
    end;
    MakeItems;
    FindSelItem(Eq);
    // Cancel := not Visible and (ItemIndex = -1);
    if Cancel or (FItems.Count = 0) or (((ItemIndex = -1) or Eq) and not ShowAlways) then
      exit;
    FPopupList.Items := FItems;
    FPopupList.ItemHeight := FItemHeight;
    FVisible := true;
    SetItemIndex(FItemIndex);
    if FListBoxStyle in [lbStandard] then
      FPopupList.Style := lbOwnerDrawFixed
    else
      FPopupList.Style := FListBoxStyle;
    FPopupList.OnMeasureItem := FMemoEx.FOnCompletionMeasureItem;
    FPopupList.OnDrawItem := FMemoEx.FOnCompletionDrawItem;

    ItemCount := FItems.Count;
    SysBorderWidth := GetSystemMetrics(SM_CXBORDER);
    SysBorderHeight := GetSystemMetrics(SM_CYBORDER);
    R := CalcCellRect(FCaretX - FLeftCol, FCaretY - FTopRow + 1);
    P := R.TopLeft;
    P.X := ClientOrigin.X + P.X;
    P.Y := ClientOrigin.Y + P.Y;
    Dec(P.X, 2 * SysBorderWidth);
    Dec(P.Y, SysBorderHeight);
    if ItemCount > FDropDownCount then
      ItemCount := FDropDownCount;
    PopupHeight := ItemHeight * ItemCount + 2;
    Y := P.Y;
    if (Y + PopupHeight) > Screen.Height then
    begin
      Y := P.Y - PopupHeight - FCellRect.Height + 1;
      if Y < 0 then
        Y := P.Y;
    end;
    PopupWidth := FDropDownWidth;
    if PopupWidth = 0 then
      PopupWidth := Width + 2 * SysBorderWidth;
  end;
  FPopupList.Left := P.X;
  FPopupList.Top := Y;
  FPopupList.Width := PopupWidth;
  FPopupList.Height := PopupHeight;
  SetWindowPos(FPopupList.Handle, HWND_TOP, P.X, Y, 0, 0, SWP_NOSIZE or SWP_NOACTIVATE or SWP_SHOWWINDOW);
  FPopupList.Visible := true;
end;

function TCompletion.Cmp1(const S1, S2: string): integer;
var
  T1, T2: string;
begin
  T1 := FMemoEx.DoChangeCase(S1, RA_CASE_CONVERT_LOWER);
  T2 := FMemoEx.DoChangeCase(S2, RA_CASE_CONVERT_LOWER);
  Result := StrLIComp(PChar(T1), PChar(T2), Length(T2));
end;

function TCompletion.Cmp2(const S1, S2: string): boolean;
var
  T1, T2: string;
begin
  T1 := FMemoEx.DoChangeCase(S1, RA_CASE_CONVERT_LOWER);
  T2 := FMemoEx.DoChangeCase(S2, RA_CASE_CONVERT_LOWER);
  Result := SameText(T1, T2);
end;

procedure TCompletion.MakeItems;
var
  i: integer;
  S: string;
begin
  FItems.Clear;
  case FMode of
    cmIdentifers:
      for i := 0 to FIdentifers.Count - 1 do
        FItems.Add(FIdentifers[i]);
    cmTemplates:
      begin
        with FMemoEx do
          S := GetWordOnPos(FLines.ParaStrings[CaretY], CaretX);
        for i := 0 to FTemplates.Count - 1 do
          if Cmp1(FTemplates[i], S) = 0 then
            FItems.Add(FTemplates[i]);
        if FItems.Count = 0 then
          FItems.Assign(FTemplates);
      end;
  end;
end;

procedure TCompletion.FindSelItem(var Eq: boolean);

  function FindFirst(Ss: TSTrings; S: string): integer;
  var
    i: integer;
  begin
    for i := 0 to Ss.Count - 1 do
      if Cmp1(Ss[i], S) = 0 then
      begin
        Result := i;
        exit;
      end;
    Result := -1;
  end;

var
  S: string;
begin
  with FMemoEx do
    if FLines.Count > 0 then
      S := GetWordOnPos(FLines.ParaStrings[CaretY], CaretX)
    else
      S := '';
  if Trim(S) = '' then
    ItemIndex := -1
  else
    ItemIndex := FindFirst(FItems, S);
  Eq := (ItemIndex > -1) and Cmp2(Trim(SubStr(FItems[ItemIndex], 0, FSeparator)), S);
end;

procedure TCompletion.SelectItem;
var
  Cancel: boolean;
  Param: boolean;
begin
  FindSelItem(Param);
  Cancel := not Visible and (ItemIndex = -1);
  case FMode of
    cmIdentifers:
      FMemoEx.DoCompletionIdentifer(Cancel);
    cmTemplates:
      FMemoEx.DoCompletionTemplate(Cancel);
  end;
  if Cancel or (GetItems.Count = 0) then
    CloseUp(false);
end;

procedure TCompletion.CloseUp(const Apply: boolean);
begin
  FItemIndex := ItemIndex;
  FPopupList.Visible := false;
  //  (FPopupList as TMemoExCompletionList). HintWindow.ReleaseHandle;
  FVisible := false;
  FTimer.Enabled := false;
  if Apply and (ItemIndex > -1) then
    case FMode of
      cmIdentifers:
        ReplaceWord(SubStr(FItems[ItemIndex], 0, FSeparator));
      cmTemplates:
        ReplaceWord(SubStr(FItems[ItemIndex], 2, FSeparator));
    end;
end;

procedure TCompletion.OnTimer(Sender: TObject);
begin
  DropDown(FDefMode, false);
end;

procedure TCompletion.ClearAutoChangeList;
var
  i: integer;
begin
  for i := 0 to FAutoChangeList.Count - 1 do
    Dispose(FAutoChangeList[i]);
  FAutoChangeList.Clear;
end;

procedure TCompletion.UpdateAutoChange;
begin
  AutoChangeChanged(FAutoChange);
end;

procedure TCompletion.AutoChangeChanged(Sender: TObject);

  procedure AddAutoChangeWord(const OldWord, NewWord: string);
  var
    ACW: PAutoChangeWord;
  begin
    if OldWord <> '' then
    begin
      New(ACW);
      ACW.OldWord := FMemoEx.DoChangeCase(OldWord, RA_CASE_CONVERT_LOWER);
      ACW.NewWord := NewWord;
      FAutoChangeList.Add(ACW);
    end;
  end;

var
  i: integer;
begin
  ClearAutoChangeList;
  for i := 0 to FAutoChange.Count - 1 do
    AddAutoChangeWord(SubStr(FAutoChange.Strings[i], 0, FSeparator), SubStr(FAutoChange.Strings[i], 1, FSeparator));
  FAutoChangeList.Sort(AutoChangeCompare);
end;

procedure TCompletion.SetStrings(index: integer; AValue: TStrings);
begin
  case index of
    0:
      FIdentifers.Assign(AValue);
    1:
      FTemplates.Assign(AValue);
    2:
      FAutoChange.Assign(AValue);
  end;
end;

function TCompletion.GetItemIndex: integer;
begin
  Result := FItemIndex;
  if FVisible then
    Result := FPopupList.ItemIndex;
end;

procedure TCompletion.SetItemIndex(AValue: integer);
begin
  FItemIndex := AValue;
  if FVisible then
    FPopupList.ItemIndex := FItemIndex;
end;

function TCompletion.GetInterval: cardinal;
begin
  Result := FTimer.Interval;
end;

procedure TCompletion.SetInterval(AValue: cardinal);
begin
  FTimer.Interval := AValue;
end;

constructor TMemoExCompletionList.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Left := -1000;
  Visible := false;
  TabStop := false;
  ParentFont := false;
  Parent := Owner as TCustomMemoEx;
  Ctl3D := false;
  FTimer := TTimer.Create(nil);
  FTimer.Enabled := false;
  FTimer.Interval := 200;
  FTimer.OnTimer := OnTimer;
  Style := lbOwnerDrawFixed;
  ItemHeight := 13;
  //  HintWindow := THintWindow.Create(Self);
end;

destructor TMemoExCompletionList.Destroy;
begin
  FTimer.Free;
  //  HintWindow.Free;
  inherited Destroy;
end;

procedure TMemoExCompletionList.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    Style := Style {or WS_POPUP}  or WS_BORDER;
    ExStyle := ExStyle or WS_EX_TOOLWINDOW;
    WindowClass.Style := WindowClass.Style or CS_SAVEBITS;
  end;
end;

procedure TMemoExCompletionList.CreateWnd;
begin
  inherited CreateWnd;
  if not (csDesigning in ComponentState) then
    Windows.SetParent(Handle, 0);
  //  CallWindowProc(DefWndProc, Handle, WM_SETFOCUS, 0, 0); {??}
end;

procedure TMemoExCompletionList.DestroyWnd;
begin
  inherited DestroyWnd;
  //  HintWindow.ReleaseHandle;
end;

procedure TMemoExCompletionList.MouseMove(Shift: TShiftState; X, Y: integer);
var
  F: integer;
begin
  YY := Y;
  // F := ItemAtPos(Point(X, Y), true);
  if KeyPressed(VK_LBUTTON) then
  begin
    F := ItemAtPos(Point(X, Y), true);
    if F > -1 then
      ItemIndex := F;
    FTimer.Enabled := (Y < 0) or (Y > ClientHeight);
    if (Y < -ItemHeight) or (Y > ClientHeight + ItemHeight) then
      FTimer.Interval := 50
    else
      FTimer.Interval := 200;
  end;
  //  if (F > -1) and not FTimer.Enabled then
  //  begin
  //    Application.CancelHint;
  //    Hint := Items[F];
  //    HintWindow.ActivateHint(Bounds(ClientOrigin.X + X, ClientOrigin.Y + Y, 300, ItemHeight), Items[F]);
  //  end;
end;

procedure TMemoExCompletionList.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
var
  F: integer;
begin
  MouseCapture := true;
  F := ItemAtPos(Point(X, Y), true);
  if F > -1 then
    ItemIndex := F;
end;

procedure TMemoExCompletionList.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
begin
  MouseCapture := false;
  (Owner as TCustomMemoEx).FCompletion.CloseUp((Button = mbLeft) and PtInRect(ClientRect, Point(X, Y)));
end;

procedure TMemoExCompletionList.OnTimer(Sender: TObject);
begin
  if (YY < 0) then
    Perform(WM_VSCROLL, SB_LINEUP, 0)
  else if (YY > ClientHeight) then
    Perform(WM_VSCROLL, SB_LINEDOWN, 0);
end;

procedure TMemoExCompletionList.WMCancelMode(var Message: TMessage);
begin
  (Owner as TCustomMemoEx).FCompletion.CloseUp(false);
end;

procedure TMemoExCompletionList.CMHintShow(var Message: TMessage);
begin
  Message.Result := 1;
end;

procedure TMemoExCompletionList.DrawItem(Index: integer; Rect: TRect; State: TOwnerDrawState);
var
  Offset, W: integer;
  S: string;
begin
  if Assigned(OnDrawItem) then
    OnDrawItem(Self, Index, Rect, State)
  else
  begin
    Canvas.FillRect(Rect);
    Offset := 3;
    with (Owner as TCustomMemoEx).FCompletion do
      case FMode of
        cmIdentifers:
          Canvas.TextOut(Rect.Left + Offset, Rect.Top, SubStr(Items[Index], 1, Separator));
        cmTemplates:
          begin
            Canvas.TextOut(Rect.Left + Offset, Rect.Top, SubStr(Items[Index], 1, Separator));
            Canvas.Font.Style := [fsBold];
            S := SubStr(Items[Index], 0, Separator);
            W := Canvas.TextWidth(S);
            Canvas.TextOut(Rect.Right - 2 * Offset - W, Rect.Top, S);
          end;
      end;
  end;
end;

{$ENDIF MEMOEX_COMPLETION}

{$ENDIF MEMOEX_EDITOR}

procedure TCustomMemoEx.ValidateEditBuffer;
begin
  if FPEditBuffer = nil then
  begin
    FEditBuffer := Lines.Text;
    FPEditBuffer := PChar(FEditBuffer);
    FEditBufferSize := Length(FEditBuffer);
  end;
end;

function TCustomMemoEx.GetText(Position: longint; Buffer: PChar; Count: longint): longint;
begin
  ValidateEditBuffer;
  if Position <= FEditBufferSize then
  begin
    Result := Min(FEditBufferSize - Position, Count);
    Move(FPEditBuffer[Position], Buffer[0], Result * SizeOf(char));
  end
  else
    Result := 0;
end;

procedure TCustomMemoEx.SetWordWrap(Value: boolean);
var
  p, x, y: integer; // AB : don't loose position
begin
  if Value <> FWordWrap then
  begin
    p := PosFromCaret(CaretX, CaretY);
    FWordWrap := Value;
    FLines.Reformat;
    CantUndo;
    CaretFromPos(p, x, y);
    SetCaret(0, Y); // X=0 to avoid new line length overflow issue
  end;
end;

procedure TCustomMemoEx.SetStripInvisible(Value: boolean);
begin
  if Value <> FStripInvisible then
  begin
    FStripInvisible := Value;
    if FReadOnly then
      Invalidate;
  end;
end;

procedure TCustomMemoEx.FontChanged(Sender: TObject);
begin
  UpdateEditorSize({fullupdate=}true);
  Invalidate;
end;

function TCustomMemoEx.GetAfterLoad: TNotifyEvent;
begin
  Result := FLines.FOnAfterLoad;
end;

procedure TCustomMemoEx.SetAfterLoad(Value: TNotifyEvent);
begin
  FLines.FOnAfterLoad := Value;
end;

function TCustomMemoEx.GetBeforeSave: TNotifyEvent;
begin
  Result := FLines.FOnBeforeSave;
end;

procedure TCustomMemoEx.SetBeforeSave(Value: TNotifyEvent);
begin
  FLines.FOnBeforeSave := Value;
end;

procedure TCustomMemoEx.SetSelectedText(Value: boolean);
begin
  if FSelected <> Value then
  begin
    FSelected := Value;
    SelectionChanged;
  end;
end;

function TCustomMemoEx.FindNext(const text: string; ignCase: boolean): boolean;
var
  X, Found, para, paraIndex: integer;
  UpText: string;
begin
  if ignCase then
    UpText := UpperCase(Text);
  SetUnSelected;
  X := FCaretX + 1; // ignore chars for first string
  FLines.Index2ParaIndex(FCaretY, para, paraIndex);
  while para < FLines.FCount do // search in all paragraphs
    with FLines.FList[para] do
    begin
      while paraIndex < FCount do
      begin // search in this paragraph
        if ignCase then
          Found := PosEx(UpText, UpperCase(FStrings[paraIndex]), X)
        else
          Found := PosEx(text, FStrings[paraIndex], X);
        if Found > 0 then
        begin
          SetSel(Found - 1, FPreCount + paraIndex);
          SetSel(FSelStartX + length(text), FSelStartY);
          SetCaret(FSelEndX, FSelEndY);
          result := true;
          exit;
        end;
        X := 1;         // now whole lines are searched
        inc(paraIndex); // next line of this paragraph
      end;
      inc(para);      // next paragraph
      paraIndex := 0; // begin with first line
    end;
  result := false;  // not found
end;

procedure TCustomMemoEx.SetTopRow(const Value: integer);
begin
  SetLeftTop(0, Value);
end;

procedure TCustomMemoEx.DoContextPopup(MousePos: TPoint; var Handled: Boolean);
var
  aPopupMenu: TPopupMenu;
begin
  if InvalidPoint(MousePos) then
  begin
    // in case of menu from keyboard -> show popupmenu at caret position
    aPopupMenu := GetPopupMenu;
    if (aPopupMenu = nil) or not aPopupMenu.AutoPopup then
      exit;
    SendCancelMode(nil);
    aPopupMenu.PopupComponent := Self;
    with ClientToScreen(CalcCellRect(FCaretX - FLeftCol, FCaretY - FTopRow).TopLeft) do
      aPopupMenu.Popup(X, Y);
    Handled := true;
  end
  else
    inherited;
end;

function TCustomMemoEx.GetTextStr: string;
begin
  result := FLines.GetTextStr;
end;

procedure TCustomMemoEx.SetTextStr(const Value: string);
begin
  FLines.SetTextStr(Value);
end;


{ TMemoEx }

class procedure TMemoEx.JSONLineAttr(Sender: TObject; const Line: string;
  index: Integer; const SelAttrs: TSelAttrs; var Attrs: TLineAttrs);
var
  i, c: integer;
  FC: TColor;
begin // limitation: work only properly if the JSON is in a single line
  FC := clWindowText;
  i := 0;
  if Line <> '' then
    repeat
      case Line[i + 1] of
      #0:
        break;
      '[', ']':
        begin
          Attrs[i].FC := clNavy;
          Attrs[i].Style := [fsBold];
          inc(i);
        end;
      '"':
        begin
          repeat
            Attrs[i].FC := clOlive;
            inc(i);
            if Line[i + 1] = #0 then
              exit;
          until (Line[i + 1] = '"') and (Line[i] <> '\');
          Attrs[i].FC := clOlive;
          inc(i);
        end;
      '-', '0'..'9':
        begin
          repeat
            Attrs[i].FC := clNavy;
            inc(i);
          until {$ifdef UNICODE} (Line[i + 1] > #127) or {$endif}
            not (AnsiChar(Line[i + 1]) in ['0'..'9', 'e', 'E', '+', '-']);
        end;
      '_', 'a'..'z', 'A'..'Z', '$':
        begin
          // see SynCommons.IsJsonIdentifierFirstChar
          {$ifdef UNICODE}
          c := ord(Line[i + 1]) + ord(Line[i + 2]) shl 8 + ord(Line[i + 3]) shl 16 + ord(Line[i + 4]) shl 24;
          {$else}
          c := PInteger(PAnsiChar(pointer(Line)) + i)^;
          {$endif}
          if (c = ord('n') + ord('u') shl 8 + ord('l') shl 16 + ord('l') shl 24) or
             (c = ord('f') + ord('a') shl 8 + ord('l') shl 16 + ord('s') shl 24) or
             (c = ord('t') + ord('r') shl 8 + ord('u') shl 16 + ord('e') shl 24) then
            repeat
              Attrs[i].FC := clNavy;
              Attrs[i].Style := [fsBold];
              inc(i);
            until {$ifdef UNICODE} (Line[i + 1] > #127) or {$endif}
              not (AnsiChar(Line[i + 1]) in ['a'..'z'])
          else
            repeat
              Attrs[i].FC := clMaroon;
              inc(i);
            until {$ifdef UNICODE} (Line[i + 1] > #127) or {$endif}
              not (AnsiChar(Line[i + 1]) in ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']']);
        end;
      else
        begin
          Attrs[i].FC := FC;
          inc(i);
        end;
      end;
    until false;
end;

procedure Register;
begin
  RegisterComponents('Standard', [TMemoEx]);
end;


initialization
  CF_MEMOEX := RegisterClipBoardFormat('CF_MEMOEX');

end.

