unit SynEditWrappedView experimental; {$mode objfpc}{$H+} interface uses Classes, SysUtils, math, LazSynEditText, SynEdit, SynEditViewedLineMap, SynEditTypes, SynEditMiscProcs, SynEditHighlighter, SynEditMiscClasses, Graphics, LazLogger, LazListClasses; type TLazSynEditLineWrapPlugin = class; TSynWordWrapLineData = Integer; PSynWordWrapLineData = ^TSynWordWrapLineData; TSynWordWrapIndexPage = class; TSynWordWrapLineMap = class; TSynWordWrapInvalidLinesRecord = record First, Last: Integer; end; PSynWordWrapInvalidLinesRecord = ^TSynWordWrapInvalidLinesRecord; { TSynWordWrapInvalidLines } TSynWordWrapInvalidLinesRecordSize = specialize TLazListClassesItemSize; TSynWordWrapInvalidLines = object(specialize TLazShiftBufferListObjBase) private function GetFirstInvalidEndLine: Integer; inline; function GetFirstInvalidLine: Integer; inline; function GetLastInvalidLine: Integer; function GetItem(AnIndex: Integer): TSynWordWrapInvalidLinesRecord; inline; function GrowCapacity(ARequired: Integer): Integer; function ShrinkCapacity(ARequired: Integer): Integer; procedure InsertRows(AIndex, ACount: Integer); inline; procedure DeleteRows(AIndex, ACount: Integer); inline; public function FindIndexFor(ALine: Integer): Integer; // find first Result.First after ALine procedure InvalidateLines(AFromOffset, AToOffset: Integer); procedure InsertInvalidateLines(AFromOffset, ACount: Integer); procedure InsertLines(AFromOffset, ACount: Integer; ASplit: Boolean = False); procedure RemoveLines(AFromOffset, ACount: Integer); procedure ValidateLines(AnOffset: Integer); inline; procedure MoveRangeAtStartTo(var ADestLines: TSynWordWrapInvalidLines; ASourceEndLine, AnAdjust: Integer); procedure MoveRangeAtEndTo(var ADestLines: TSynWordWrapInvalidLines; ASourceStartLine, AnAdjust: Integer); property FirstInvalidLine: Integer read GetFirstInvalidLine; property FirstInvalidEndLine: Integer read GetFirstInvalidEndLine; property LastInvalidLine: Integer read GetLastInvalidLine; property Item[AnIndex: Integer]: TSynWordWrapInvalidLinesRecord read GetItem; end; { TSynWordWrapLineMap } TSynWordWrapLineMap = class private FAvlNode: TSynWordWrapIndexPage; FInvalidLines: TSynWordWrapInvalidLines; FDeferredAdjustFromOffs, FDeferredAdjustFromVal: Integer; (* FWrappedExtraSums: Sum of all extra lines due to wrapping up to (and including) the current element. If a line wraps and needs 3 viewed-lines, then that is 2 extra lines - WrappedOffsetFor(n) => Viewed start of line n - FWrappedExtraSums[n] => Viewed start of line n+1 *) FWrappedExtraSumsCount: Integer; FWrappedExtraSums: Array of TSynWordWrapLineData; // -1 based FOffsetAtStart: Integer; private function GetCapacity: Integer; inline; procedure SetCapacity(AValue: Integer); inline; procedure GrowCapacity(ARequired: Integer); procedure ShrinkCapacity; private function GetViewedCount: Integer; function GetViewedRealCountDifference: Integer; function GetWrappedExtraSumBefore(ARealOffset: Integer): Integer; inline; // Must have: ARealOffset < FWrappedExtraSumsCount // ingnores FOffsetAtStart function GetWrappedOffsetFor(ARealOffset: IntIdx): IntIdx; inline; function GetFirstInvalidLine: Integer; inline; function GetFirstInvalidEndLine: Integer; inline; function GetLastInvalidLine: Integer; inline; procedure AddToInvalidList; inline; procedure RemoveFromInvalidList(AMode: TRemoveFromInvalidListMode = rfiDefault); inline; procedure MaybeUpdateViewedSizeDifference; inline; property Capacity: Integer read GetCapacity write SetCapacity; public constructor Create; destructor Destroy; override; function GetDumpData: String; property AvlNode: TSynWordWrapIndexPage read FAvlNode; property Offset: Integer read FOffsetAtStart; property RealCount: Integer read FWrappedExtraSumsCount; property ViewedCount: Integer read GetViewedCount; property ViewedRealCountDifference: Integer read GetViewedRealCountDifference; // viewed - real procedure InvalidateLines(AFromOffset, AToOffset: Integer); // procedure ValidateLine(ALineOffset, AWrappCount: Integer); procedure EndValidate; property FirstInvalidLine: Integer read GetFirstInvalidLine; property FirstInvalidEndLine: Integer read GetFirstInvalidEndLine; property LastInvalidLine: Integer read GetLastInvalidLine; procedure InsertLinesAtOffset(ALineOffset, ALineCount: Integer); procedure DeleteLinesAtOffset(ALineOffset, ALineCount: Integer; ADoNotShrink: Boolean = False); procedure MoveLinesAtStartTo(ADestPage: TSynWordWrapLineMap; ASourceEndLine, ATargetStartLine: Integer); procedure MoveLinesAtEndTo(ADestPage: TSynWordWrapLineMap; ASourceStartLine, ALineCount: Integer); function GetOffsetForWrap(AViewedOffset: IntIdx; out ASubOffset: IntIdx): IntIdx; property WrappedOffsetFor[ARealOffset: IntIdx]: IntIdx read GetWrappedOffsetFor; end; { TSynWordWrapIndexPage } TSynWordWrapIndexPage = class(TSynEditLineMapPage) private FSynWordWrapLineMap: TSynWordWrapLineMap; FSynEditWrappedPlugin :TLazSynEditLineWrapPlugin; procedure UpdateViewedSizeDifference; procedure MaybeJoinWithSibling; protected function GetFirstInvalidLine: Integer; override; function GetFirstInvalidEndLine: Integer; override; function GetLastInvalidLine: Integer; override; function GetViewedRealCountDifference: Integer; override; function GetWrappedOffsetFor(ARealOffset: IntIdx): IntIdx; override; function IsValid: boolean; override; public property SynWordWrapLineMapStore: TSynWordWrapLineMap read FSynWordWrapLineMap; experimental; // 'For test case only'; public constructor Create(ATree: TSynLineMapAVLTree); override; destructor Destroy; override; procedure DumpNode(ALine: Integer = 0; AnIndent: Integer = 0); override; function CanExtendStartTo(ALineOffs: Integer; AIgnoreJoinDist: Boolean = False): boolean; override; function CanExtendEndTo(ALineOffs: Integer; AIgnoreJoinDist: Boolean = False): boolean; override; procedure InsertLinesAtOffset(ALineOffset, ALineCount: IntIdx); override; procedure DeleteLinesAtOffset(ALineOffset, ALineCount: IntIdx; ADoNotShrink: Boolean = False); override; procedure AdjustForLinesInserted(AStartLine, ALineCount: IntIdx; ABytePos: Integer); override; procedure AdjustForLinesDeleted(AStartLine, ALineCount: IntIdx; ABytePos: Integer); override; procedure MoveLinesAtStartTo(ADestPage: TSynEditLineMapPage; ASourceEndLine, ATargetStartLine: Integer); override; procedure MoveLinesAtEndTo(ADestPage: TSynEditLineMapPage; ASourceStartLine, ACount: Integer); override; // must be FirstInvalidLine (or Last) => so FirstInvalidLine can be set. procedure EndValidate; override; procedure ValidateLine(ALineOffset, AWrappCount: Integer); override; procedure InvalidateLines(AFromOffset, AToOffset: Integer); override; // TODO: adjust offset function ExtendAndInvalidateLines(AFromLineIdx, AToLineIdx: TLineIdx): Boolean; override; function RealCount: Integer; override; // count of real lines function RealStartLine: Integer; override; // Offset function RealEndLine: Integer; override; // Offset + RealCount - 1; function GetOffsetForWrap(AWrapOffset: IntIdx; out ASubOffset: IntIdx): IntIdx; override; function TextXYIdxToViewXYIdx(ATextXYIdx: TPhysPoint; ANodeStartLine: IntIdx): TPhysPoint; override; function ViewXYIdxToTextXYIdx(AViewXYIdx: TPhysPoint; ANodeStartLine: IntIdx): TPhysPoint; override; end; TLazSynEditWrapCaretPos = (wcpEOL, wcpBOL); { TLazSynDisplayWordWrap } TLazSynDisplayWordWrap = class(TLazSynDisplayLineMapping) private FWrapPlugin: TLazSynEditLineWrapPlugin; FCurSubLineLogStartIdx, FCurSubLineNextLogStartIdx, FCurSubLinePhysStartIdx: Integer; FCurToken: TLazSynDisplayTokenInfo; FCurLineLogIdx: Integer; public constructor Create(AWrappedView: TSynEditLineMappingView; AWrapPlugin: TLazSynEditLineWrapPlugin); //destructor Destroy; override; procedure SetHighlighterTokensLine(AWrappedLine: TLineIdx; out ARealLine: TLineIdx; out AStartBytePos, ALineByteLen: Integer); override; function GetNextHighlighterToken(out ATokenInfo: TLazSynDisplayTokenInfo): Boolean; override; end; { TLazSynEditLineWrapPlugin } TLazSynEditLineWrapPlugin = class(TLazSynEditPlugin) private FCaretWrapPos: TLazSynEditWrapCaretPos; procedure DoLinesChanged(Sender: TObject); procedure DoWidthChanged(Sender: TObject; Changes: TSynStatusChanges); function GetWrapColumn: Integer; public FLineMapView: TSynEditLineMappingView; function CreatePageMapNode(AMapTree: TSynLineMapAVLTree ): TSynEditLineMapPage; protected procedure SetEditor(const AValue: TCustomSynEdit); override; function CalculateNextBreak(ALine: PChar; ALogStartFrom: IntIdx; AMaxWidth: Integer; const PhysCharWidths: TPhysicalCharWidths; out APhysWidth: Integer): IntIdx; function GetSublineCount (ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths): Integer; inline; procedure GetSublineBounds(ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths; ASubLine: Integer; out ALogStartX, ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer); function GetSubLineFromX (ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths; var APhysXPos: Integer): integer; procedure GetWrapInfoForViewedXY(var AViewedXY: TPhysPoint; AFlags: TViewedXYInfoFlags; out AFirstViewedX: IntPos; ALogPhysConvertor: TSynLogicalPhysicalConvertor); function TextXYToLineXY(ATextXY: TPhysPoint): TPhysPoint; function LineXYToTextX(ARealLine: IntPos; ALineXY: TPhysPoint): Integer; function CalculateWrapForLine(ALineIdx: IntIdx; AMaxWidth: integer): Integer; inline; public constructor Create(AOwner: TComponent); override; procedure WrapAll; experimental; procedure ValidateAll; experimental; property CaretWrapPos: TLazSynEditWrapCaretPos read FCaretWrapPos write FCaretWrapPos; property WrapColumn: Integer read GetWrapColumn; end; implementation { TSynWordWrapInvalidLines } function TSynWordWrapInvalidLines.GetFirstInvalidEndLine: Integer; begin if Count = 0 then Result := -1 else Result := Item[0].Last; end; function TSynWordWrapInvalidLines.GetFirstInvalidLine: Integer; begin if Count = 0 then Result := -1 else Result := Item[0].First; end; function TSynWordWrapInvalidLines.GetLastInvalidLine: Integer; var i: Integer; begin i := Count; if i = 0 then Result := -1 else Result := Item[i-1].Last; end; function TSynWordWrapInvalidLines.GetItem(AnIndex: Integer): TSynWordWrapInvalidLinesRecord; begin Result := ItemPointer[AnIndex]^; end; function TSynWordWrapInvalidLines.GrowCapacity(ARequired: Integer): Integer; begin Result := ARequired + 16; end; function TSynWordWrapInvalidLines.ShrinkCapacity(ARequired: Integer): Integer; begin //if ARequired = 0 then // Result := 0 //else if ARequired * 8 < Count then Result := ARequired + 4 else Result := -1; end; procedure TSynWordWrapInvalidLines.InsertRows(AIndex, ACount: Integer); begin InsertRowsEx(AIndex, ACount, @GrowCapacity); end; procedure TSynWordWrapInvalidLines.DeleteRows(AIndex, ACount: Integer); begin DeleteRowsEx(AIndex, ACount, @ShrinkCapacity); end; function TSynWordWrapInvalidLines.FindIndexFor(ALine: Integer): Integer; var l, h: integer; begin l := 0; h := Count-1; if (h < 0) then begin Result := 0; exit; end; Result := (l + h) div 2; while (h > l) do begin if PSynWordWrapInvalidLinesRecord(ItemPointer[Result])^.First >= ALine then h := Result else l := Result + 1; Result := cardinal(l + h) div 2; end; if PSynWordWrapInvalidLinesRecord(ItemPointer[Result])^.First < ALine then inc(Result); end; procedure TSynWordWrapInvalidLines.InvalidateLines(AFromOffset, AToOffset: Integer); var i, j, c: Integer; begin c := Count; i := FindIndexFor(AFromOffset); if (i > 0) and (Item[i-1].Last >= AFromOffset-1) then begin if (Item[i-1].Last >= AToOffset) then exit; dec(i); PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := AToOffset; end else if (i < c) and (Item[i].First = AToOffset+1) then begin PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.First := AFromOffset; if AToOffset > PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last then PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := AToOffset; end else begin assert((i = 0) or (Item[i-1].Last < AFromOffset-1), 'TSynWordWrapInvalidLines.InvalidateLines: (i = 0) or (Item[i-1].Last < AFromOffset-1)'); assert((i >= c -1) or (Item[i+1].First > AToOffset+1), 'TSynWordWrapInvalidLines.InvalidateLines: (i < Cnt-1) or (Item[i+1].First > AToOffset+1)'); InsertRows(i, 1); PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.First := AFromOffset; PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := AToOffset; inc(c); end; j := i + 1; while j < c do begin if (Item[j].Last <= AToOffset) then begin DeleteRows(j,1); dec(c); end else if (Item[j].First <= AToOffset+1) then begin PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := Item[j].Last; DeleteRows(j,1); dec(c); end else break; end; end; procedure TSynWordWrapInvalidLines.InsertInvalidateLines(AFromOffset, ACount: Integer); begin InsertLines(AFromOffset, ACount); InvalidateLines(AFromOffset, AFromOffset + ACount - 1); end; procedure TSynWordWrapInvalidLines.InsertLines(AFromOffset, ACount: Integer; ASplit: Boolean); var i, j: Integer; begin i := Count; while i > 0 do begin dec(i); j := Item[i].First; if j >= AFromOffset then begin PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.First := j + ACount; PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := Item[i].Last + ACount; end else if Item[i].Last >= AFromOffset then begin if ASplit then begin j := PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last; PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := AFromOffset - 1; InsertInvalidateLines(AFromOffset+ACount, j-AFromOffset+ 1); end else PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := Item[i].Last + ACount; end else begin assert((Item[i].Last < AFromOffset), 'TSynWordWrapInvalidLines.InsertInvalidateLines: (Item[i].Last < AFromOffset)'); break; end; end; end; procedure TSynWordWrapInvalidLines.RemoveLines(AFromOffset, ACount: Integer); var i, f, l, k: Integer; begin i := Count; while i > 0 do begin dec(i); f := ItemPointer[i]^.First; l := ItemPointer[i]^.Last; if f >= AFromOffset then begin k := Max(AFromOffset, f - ACount); if l - ACount < k then begin DeleteRows(i,1); end else begin ItemPointer[i]^.First := k; ItemPointer[i]^.Last := l - ACount; end; end else if l >= AFromOffset then begin k := Max(AFromOffset - 1, l - ACount); assert(k >= f, 'TSynWordWrapInvalidLines.RemoveLines: k >= f'); PSynWordWrapInvalidLinesRecord(ItemPointer[i])^.Last := k; end else begin break; end; end; end; procedure TSynWordWrapInvalidLines.ValidateLines(AnOffset: Integer); var i, c: Integer; begin assert((AnOffset >= 0) and (AnOffset = FirstInvalidLine) or (AnOffset = LastInvalidLine), 'TSynWordWrapInvalidLines.ValidateLines: (AnOffset >= 0) and (AnOffset = FirstInvalidLine) or (AnOffset = LastInvalidLine)'); i := Item[0].First; if AnOffset = i then begin if i < Item[0].Last then PSynWordWrapInvalidLinesRecord(ItemPointer[0])^.First := i + 1 else DeleteRows(0,1); end else begin c := Count-1; i := Item[c].last; if i > Item[c].First then PSynWordWrapInvalidLinesRecord(ItemPointer[c])^.Last := i - 1 else DeleteRows(c,1); end; end; procedure TSynWordWrapInvalidLines.MoveRangeAtStartTo( var ADestLines: TSynWordWrapInvalidLines; ASourceEndLine, AnAdjust: Integer); var i, c, ItemFirst, ItemLast: Integer; DelTo: Integer; begin DelTo := -1; c := Count; i := 0; while i < c do begin ItemFirst := ItemPointer[i]^.First; ItemLast := ItemPointer[i]^.Last; if ItemLast <= ASourceEndLine then begin ADestLines.InvalidateLines(ItemFirst + AnAdjust, ItemLast + AnAdjust); DelTo := i; end else if ItemFirst <= ASourceEndLine then begin ADestLines.InvalidateLines(ItemFirst + AnAdjust, ASourceEndLine + AnAdjust); ItemPointer[i]^.First := ASourceEndLine; break; end else break; inc(i); end; if DelTo >= 0 then DeleteRows(0, DelTo + 1); end; procedure TSynWordWrapInvalidLines.MoveRangeAtEndTo( var ADestLines: TSynWordWrapInvalidLines; ASourceStartLine, AnAdjust: Integer); var i, ItemFirst, ItemLast: Integer; DelFrom: Integer; begin i := Count; DelFrom := i; while i > 0 do begin dec(i); ItemFirst := ItemPointer[i]^.First; ItemLast := ItemPointer[i]^.Last; if ItemFirst >= ASourceStartLine then begin ADestLines.InvalidateLines(ItemFirst + AnAdjust, ItemLast + AnAdjust); DelFrom := i; end else if ItemLast >= ASourceStartLine then begin ADestLines.InvalidateLines(ASourceStartLine + AnAdjust, ItemLast + AnAdjust); ItemPointer[i]^.Last := ASourceStartLine - 1; break; end else break; end; if DelFrom < Count then DeleteRows(DelFrom, Count - DelFrom); end; procedure WrapInfoFillFrom(ATarget: PSynWordWrapLineData; ACount, AValue: Integer); inline; var i: Integer; begin for i := 0 to ACount - 1 do begin ATarget^ := AValue; inc(ATarget); end; end; procedure WrapInfoCopyFromTo(ASource, ATarget: PSynWordWrapLineData; ACount: Integer); inline; var i: Integer; begin for i := 0 to ACount - 1 do begin ATarget^ := ASource^; inc(ASource); inc(ATarget); end; end; procedure WrapInfoMoveUpFromTo(ASource, ATarget: PSynWordWrapLineData; ACount: Integer); inline; var i: Integer; begin inc(ASource, ACount - 1); inc(ATarget, ACount - 1); for i := 0 to ACount - 1 do begin ATarget^ := ASource^; dec(ASource); dec(ATarget); end; end; procedure WrapInfoCopyAndAdjustFromTo(ASource, ATarget: PSynWordWrapLineData; ACount, AnAdjustVal: Integer); inline; var i: Integer; begin for i := 0 to ACount - 1 do begin ATarget^ := ASource^ + AnAdjustVal; inc(ASource); inc(ATarget); end; end; procedure WrapInfoMoveUpAndAdjustFromTo(ASource, ATarget: PSynWordWrapLineData; ACount, AnAdjustVal: Integer); inline; var i: Integer; begin inc(ASource, ACount - 1); inc(ATarget, ACount - 1); for i := 0 to ACount - 1 do begin ATarget^ := ASource^ + AnAdjustVal; dec(ASource); dec(ATarget); end; end; { TSynWordWrapLineMap } function TSynWordWrapLineMap.GetCapacity: Integer; begin Result := Length(FWrappedExtraSums); end; procedure TSynWordWrapLineMap.SetCapacity(AValue: Integer); begin if AValue < FWrappedExtraSumsCount then AValue := FWrappedExtraSumsCount; SetLength(FWrappedExtraSums, AValue); end; procedure TSynWordWrapLineMap.GrowCapacity(ARequired: Integer); begin if Capacity < ARequired then Capacity := ARequired + SYN_WORD_WRAP_GROW_SIZE; end; procedure TSynWordWrapLineMap.ShrinkCapacity; var i: Integer; begin i := 0; while (i < FWrappedExtraSumsCount) and (FWrappedExtraSums[i] = 0) do inc(i); if i > 0 then begin WrapInfoCopyFromTo( @FWrappedExtraSums[i], @FWrappedExtraSums[0], FWrappedExtraSumsCount-i); FOffsetAtStart := FOffsetAtStart + i; FWrappedExtraSumsCount := FWrappedExtraSumsCount - i; end; if Capacity > FWrappedExtraSumsCount + 2 * SYN_WORD_WRAP_GROW_SIZE then Capacity := FWrappedExtraSumsCount + SYN_WORD_WRAP_GROW_SIZE; end; function TSynWordWrapLineMap.GetViewedCount: Integer; begin assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.GetViewedCount: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); Result := FWrappedExtraSumsCount; if FWrappedExtraSumsCount > 0 then Result := Result + FWrappedExtraSums[FWrappedExtraSumsCount - 1]; end; function TSynWordWrapLineMap.GetViewedRealCountDifference: Integer; begin assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.GetViewedRealCountDifference: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); if FWrappedExtraSumsCount > 0 then Result := FWrappedExtraSums[FWrappedExtraSumsCount - 1] else Result := 0; end; function TSynWordWrapLineMap.GetWrappedExtraSumBefore(ARealOffset: Integer ): Integer; begin if ARealOffset = 0 then Result := 0 else Result := FWrappedExtraSums[ARealOffset-1]; end; function TSynWordWrapLineMap.GetWrappedOffsetFor(ARealOffset: IntIdx): IntIdx; begin Result := ARealOffset; if ARealOffset <= FOffsetAtStart then exit; ARealOffset := ARealOffset - FOffsetAtStart; if ARealOffset > FWrappedExtraSumsCount then begin if FWrappedExtraSumsCount > 0 then Result := Result + FWrappedExtraSums[FWrappedExtraSumsCount - 1]; end else if ARealOffset > 0 then Result := Result + FWrappedExtraSums[ARealOffset - 1]; end; procedure TSynWordWrapLineMap.AddToInvalidList; begin FAvlNode.AddToInvalidList; end; procedure TSynWordWrapLineMap.RemoveFromInvalidList( AMode: TRemoveFromInvalidListMode); begin FAvlNode.RemoveFromInvalidList(AMode); end; procedure TSynWordWrapLineMap.MaybeUpdateViewedSizeDifference; begin if FirstInvalidLine < 0 then FAvlNode.UpdateViewedSizeDifference; end; procedure TSynWordWrapLineMap.InvalidateLines(AFromOffset, AToOffset: Integer); begin assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.InvalidateLines: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); FInvalidLines.InvalidateLines(AFromOffset, AToOffset); AddToInvalidList; end; procedure TSynWordWrapLineMap.ValidateLine(ALineOffset, AWrappCount: Integer); var i, j: Integer; begin assert(ALineOffset >= 0, 'TSynWordWrapLineMap.ValidateLine: ALineOffset >= 0'); assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.ValidateLine: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); FInvalidLines.ValidateLines(ALineOffset); if ALineOffset - FOffsetAtStart < FDeferredAdjustFromOffs then begin EndValidate; end else if FDeferredAdjustFromOffs > 0 then begin j := FDeferredAdjustFromVal; //AWrappCount := AWrappCount + j; for i := FDeferredAdjustFromOffs to Min(FWrappedExtraSumsCount - 1, ALineOffset - FOffsetAtStart) do FWrappedExtraSums[i] := FWrappedExtraSums[i] + j; FDeferredAdjustFromOffs := 0; end; if ALineOffset < FOffsetAtStart then begin if AWrappCount <> 1 then begin i := FirstInvalidLine; if (i < 0) or (ALineOffset < i) then i := ALineOffset; j := FOffsetAtStart - i; GrowCapacity(FWrappedExtraSumsCount + j); WrapInfoMoveUpAndAdjustFromTo( @FWrappedExtraSums[0], @FWrappedExtraSums[j], FWrappedExtraSumsCount, AWrappCount - 1); WrapInfoFillFrom( @FWrappedExtraSums[i - ALineOffset], j - (i - ALineOffset), AWrappCount - 1); assert(FOffsetAtStart >= j, 'TSynWordWrapLineMap.ValidateLine: FOffsetAtStart >= j'); FOffsetAtStart := FOffsetAtStart - j; FWrappedExtraSumsCount := FWrappedExtraSumsCount + j; end; end else begin ALineOffset := ALineOffset - FOffsetAtStart; if ALineOffset >= FWrappedExtraSumsCount then begin if AWrappCount > 1 then begin i := FWrappedExtraSumsCount; // TODO: check LastInvalidLine and SYN_WORD_WRAP_SPLIT_SIZE; GrowCapacity(ALineOffset + 1); FWrappedExtraSumsCount := ALineOffset + 1; WrapInfoFillFrom( @FWrappedExtraSums[i], FWrappedExtraSumsCount - i, GetWrappedExtraSumBefore(i)); FWrappedExtraSums[ALineOffset] := AWrappCount - 1; // last element, no AdjustFrom() needed if ALineOffset > 0 then FWrappedExtraSums[ALineOffset] := FWrappedExtraSums[ALineOffset] + FWrappedExtraSums[ALineOffset-1]; end; end else if (AWrappCount = 1) and (ALineOffset = FWrappedExtraSumsCount - 1) then begin if ALineOffset = 0 then begin FWrappedExtraSumsCount := 0; FOffsetAtStart := 0; end else begin // TODO: defer if there are further invalid lines after ALineOffset i := ALineOffset - 1; // i = FWrappedExtraSumsCount - 2 j := FWrappedExtraSums[i]; if j = 0 then begin FWrappedExtraSumsCount := 0; FOffsetAtStart := 0; end else begin dec(i); while (i >= 0) and (j = FWrappedExtraSums[i]) do dec(i); FWrappedExtraSumsCount := i + 2; end; end; end else begin j := AWrappCount - 1 + GetWrappedExtraSumBefore(ALineOffset); FDeferredAdjustFromOffs := ALineOffset + 1; FDeferredAdjustFromVal := FDeferredAdjustFromVal + j - FWrappedExtraSums[ALineOffset]; FWrappedExtraSums[ALineOffset] := j; end; end; end; procedure TSynWordWrapLineMap.EndValidate; var v, i: Integer; begin if FDeferredAdjustFromOffs > 0 then begin v := FDeferredAdjustFromVal; for i := FDeferredAdjustFromOffs to FWrappedExtraSumsCount - 1 do FWrappedExtraSums[i] := FWrappedExtraSums[i] + v; end; FDeferredAdjustFromOffs := 0; FDeferredAdjustFromVal := 0; if (FInvalidLines.Count = 0) then begin FAvlNode.UpdateViewedSizeDifference; RemoveFromInvalidList; ShrinkCapacity; end; end; procedure TSynWordWrapLineMap.MoveLinesAtStartTo(ADestPage: TSynWordWrapLineMap; ASourceEndLine, ATargetStartLine: Integer); var MinLineCount, TrgO1: Integer; W: TSynWordWrapLineData; begin assert(ATargetStartLine >= ADestPage.FWrappedExtraSumsCount + ADestPage.FOffsetAtStart, 'TSynWordWrapLineMap.InsertLinesFromPage: ATargetStartLine > ADestPage.FWrappedExtraSumsCount + ADestPage.FOffsetAtStart'); FInvalidLines.MoveRangeAtStartTo(ADestPage.FInvalidLines, ASourceEndLine, ATargetStartLine); if (FWrappedExtraSumsCount = 0) then exit; ASourceEndLine := ASourceEndLine - Offset; if ASourceEndLine < 0 then exit; if (ASourceEndLine > 0) and (ASourceEndLine < FWrappedExtraSumsCount) then begin W := FWrappedExtraSums[ASourceEndLine]; while (ASourceEndLine > 0) and (FWrappedExtraSums[ASourceEndLine - 1] = W) do dec(ASourceEndLine); end; if ADestPage.FWrappedExtraSumsCount = 0 then begin // Target page is empty ADestPage.FOffsetAtStart := ATargetStartLine + Offset; assert(ADestPage.FOffsetAtStart >= 0, 'TSynWordWrapLineMap.MoveLinesAtStartTo: ADestPage.FOffsetAtStart >= 0'); MinLineCount := Min(ASourceEndLine+1, FWrappedExtraSumsCount); ADestPage.GrowCapacity(MinLineCount); WrapInfoCopyFromTo( @FWrappedExtraSums[0], @ADestPage.FWrappedExtraSums[0], MinLineCount); ADestPage.FWrappedExtraSumsCount := MinLineCount; ADestPage.MaybeUpdateViewedSizeDifference; exit; end; ATargetStartLine := ATargetStartLine + Offset - ADestPage.FOffsetAtStart; MinLineCount := Min(ASourceEndLine+1, FWrappedExtraSumsCount); TrgO1 := ADestPage.GetWrappedExtraSumBefore(ADestPage.FWrappedExtraSumsCount); ADestPage.GrowCapacity(ATargetStartLine + MinLineCount); if ATargetStartLine > ADestPage.FWrappedExtraSumsCount then begin WrapInfoFillFrom( @ADestPage.FWrappedExtraSums[ADestPage.FWrappedExtraSumsCount], ATargetStartLine - ADestPage.FWrappedExtraSumsCount, TrgO1); end; WrapInfoCopyAndAdjustFromTo( @FWrappedExtraSums[0], @ADestPage.FWrappedExtraSums[ATargetStartLine], MinLineCount, TrgO1); ADestPage.FWrappedExtraSumsCount := ATargetStartLine + MinLineCount; ADestPage.MaybeUpdateViewedSizeDifference; end; procedure TSynWordWrapLineMap.MoveLinesAtEndTo(ADestPage: TSynWordWrapLineMap; ASourceStartLine, ALineCount: Integer); var OldOffset, SrcO1, SrcO2, MinLineCount: Integer; W: TSynWordWrapLineData; begin assert(ASourceStartLine-FOffsetAtStart+ALineCount >= FWrappedExtraSumsCount, 'TSynWordWrapLineMap.MoveLinesAtEndTo: ASourceStartLine+ACount >= FWrappedExtraSumsCount'); ADestPage.FInvalidLines.InsertLines(0, ALineCount); FInvalidLines.MoveRangeAtEndTo(ADestPage.FInvalidLines, ASourceStartLine, -ASourceStartLine); if (FWrappedExtraSumsCount = 0) then begin if ADestPage.FWrappedExtraSumsCount = 0 then exit; ADestPage.FOffsetAtStart := ADestPage.FOffsetAtStart + ALineCount; assert(ADestPage.FOffsetAtStart >= 0, 'TSynWordWrapLineMap.MoveLinesAtEndTo: ADestPage.FOffsetAtStart >= 0'); exit; end; OldOffset := ADestPage.FOffsetAtStart; ADestPage.FOffsetAtStart := 0; if ASourceStartLine < Offset then begin ADestPage.FOffsetAtStart := Offset - ASourceStartLine; ASourceStartLine := 0; ALineCount := ALineCount - ADestPage.FOffsetAtStart; if ALineCount = 0 then begin ADestPage.FOffsetAtStart := ADestPage.FOffsetAtStart + OldOffset; assert(ADestPage.FOffsetAtStart >= 0, 'TSynWordWrapLineMap.MoveLinesAtEndTo: ADestPage.FOffsetAtStart >= 0'); exit; end; end else ASourceStartLine := ASourceStartLine - Offset; if (ASourceStartLine > 0) and (ASourceStartLine < FWrappedExtraSumsCount) then begin SrcO2 := ASourceStartLine; W := FWrappedExtraSums[ASourceStartLine - 1]; while (ASourceStartLine < FWrappedExtraSumsCount) and (FWrappedExtraSums[ASourceStartLine] = W) do inc(ASourceStartLine); ALineCount := ALineCount + SrcO2 - ASourceStartLine; ADestPage.FOffsetAtStart := ADestPage.FOffsetAtStart + ASourceStartLine - SrcO2; if ALineCount <= 0 then exit; end; SrcO1 := GetWrappedExtraSumBefore(Min(ASourceStartLine, FWrappedExtraSumsCount)); MinLineCount := Max(0, Min(ALineCount, FWrappedExtraSumsCount - ASourceStartLine)); if ADestPage.FWrappedExtraSumsCount = 0 then begin // Moving to an empty page. Do NOT include any lines after FWrappedExtraSumsCount if MinLineCount > 0 then begin; ADestPage.GrowCapacity(MinLineCount); WrapInfoCopyAndAdjustFromTo( @FWrappedExtraSums[ASourceStartLine], @ADestPage.FWrappedExtraSums[0], MinLineCount, -SrcO1); end; ADestPage.FWrappedExtraSumsCount := MinLineCount; ADestPage.MaybeUpdateViewedSizeDifference; exit; end; SrcO2 := GetWrappedExtraSumBefore(Min(ASourceStartLine + ALineCount, FWrappedExtraSumsCount)); ADestPage.GrowCapacity(ADestPage.FWrappedExtraSumsCount + ALineCount + OldOffset); WrapInfoMoveUpAndAdjustFromTo( @ADestPage.FWrappedExtraSums[0], @ADestPage.FWrappedExtraSums[ALineCount + OldOffset], ADestPage.FWrappedExtraSumsCount, SrcO2 - SrcO1); if MinLineCount > 0 then WrapInfoCopyAndAdjustFromTo( @FWrappedExtraSums[ASourceStartLine], @ADestPage.FWrappedExtraSums[0], MinLineCount, -SrcO1); WrapInfoFillFrom( @ADestPage.FWrappedExtraSums[MinLineCount], ALineCount - MinLineCount + OldOffset, SrcO2 - SrcO1); ADestPage.FWrappedExtraSumsCount := ADestPage.FWrappedExtraSumsCount + ALineCount + OldOffset; ADestPage.MaybeUpdateViewedSizeDifference; end; procedure TSynWordWrapLineMap.InsertLinesAtOffset(ALineOffset, ALineCount: Integer); var j, k: Integer; begin assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.InsertLinesAtOffset: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); if ALineCount = 0 then exit; FInvalidLines.InsertInvalidateLines(ALineOffset, ALineCount); AddToInvalidList; if ALineOffset <= FOffsetAtStart then begin if FWrappedExtraSumsCount > 0 then FOffsetAtStart := FOffsetAtStart + ALineCount; assert(FOffsetAtStart >= 0, 'TSynWordWrapLineMap.MoveLinesAtEndTo: FOffsetAtStart >= 0'); exit; end; ALineOffset := ALineOffset - FOffsetAtStart; if ALineOffset < FWrappedExtraSumsCount then begin GrowCapacity(FWrappedExtraSumsCount + ALineCount); move(FWrappedExtraSums[ALineOffset], FWrappedExtraSums[ALineOffset+ALineCount], sizeof(FWrappedExtraSums[0]) * (FWrappedExtraSumsCount - ALineOffset)); FWrappedExtraSumsCount := FWrappedExtraSumsCount + ALineCount; j := GetWrappedExtraSumBefore(ALineOffset); for k := ALineOffset to ALineOffset + ALineCount - 1 do FWrappedExtraSums[k] := j; // TODO: only if NO invalid lines? FAvlNode.UpdateViewedSizeDifference; end; end; procedure TSynWordWrapLineMap.DeleteLinesAtOffset(ALineOffset, ALineCount: Integer; ADoNotShrink: Boolean); var i, j: Integer; begin assert((FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0), 'TSynWordWrapLineMap.DeleteLinesAtOffset: (FOffsetAtStart = 0) or (FWrappedExtraSumsCount > 0)'); if ALineCount = 0 then exit; FInvalidLines.RemoveLines(ALineOffset, ALineCount); if ALineOffset < FOffsetAtStart then begin i := Min(ALineCount, FOffsetAtStart - ALineOffset); FOffsetAtStart := FOffsetAtStart -i; assert(FOffsetAtStart >= 0, 'TSynWordWrapLineMap.MoveLinesAtEndTo: FOffsetAtStart >= 0'); ALineCount := ALineCount - i; if ALineCount = 0 then exit; ALineOffset := ALineOffset - FOffsetAtStart; end else ALineOffset := ALineOffset - FOffsetAtStart; if ALineOffset < FWrappedExtraSumsCount then begin ALineCount := Min(ALineCount, FWrappedExtraSumsCount - ALineOffset); WrapInfoCopyAndAdjustFromTo( @FWrappedExtraSums[ALineOffset + ALineCount], @FWrappedExtraSums[ALineOffset], FWrappedExtraSumsCount - (ALineCount + ALineOffset), GetWrappedExtraSumBefore(ALineOffset) - FWrappedExtraSums[ALineOffset + ALineCount - 1] ); FWrappedExtraSumsCount := FWrappedExtraSumsCount - ALineCount; if (ALineOffset > 0) and (ALineOffset = FWrappedExtraSumsCount) then begin i := FWrappedExtraSumsCount - 1; j := FWrappedExtraSums[i]; dec(i); while (i >= 0) and (j = FWrappedExtraSums[i]) do dec(i); inc(i); if i < LastInvalidLine then i := LastInvalidLine; inc(i); if i < FWrappedExtraSumsCount then begin FWrappedExtraSumsCount := i; end; end; if FWrappedExtraSumsCount = 0 then FOffsetAtStart := 0; end; if (FInvalidLines.Count = 0) then begin ShrinkCapacity; RemoveFromInvalidList; end; // TODO: only if NO invalid lines? FAvlNode.UpdateViewedSizeDifference; end; function TSynWordWrapLineMap.GetOffsetForWrap(AViewedOffset: IntIdx; out ASubOffset: IntIdx): IntIdx; var l, h: Integer; begin Result := 0; ASubOffset := 0; if (FWrappedExtraSumsCount = 0) or (AViewedOffset <= FOffsetAtStart) then exit(AViewedOffset); AViewedOffset := AViewedOffset - FOffsetAtStart; if AViewedOffset >= FWrappedExtraSums[FWrappedExtraSumsCount - 1] + FWrappedExtraSumsCount then exit(AViewedOffset - FWrappedExtraSums[FWrappedExtraSumsCount - 1] + FOffsetAtStart); l := 0; h := FWrappedExtraSumsCount - 1; Result := FWrappedExtraSumsCount div 2; while h > l do begin if FWrappedExtraSums[Result]+Result >= AViewedOffset then h := Result else l := Result + 1; Result := (h+l) div 2; end; assert(h=l, 'TSynWordWrapLineMap.GetOffsetForWrap: h=l'); if Result = 0 then ASubOffset := AViewedOffset else ASubOffset := AViewedOffset - FWrappedExtraSums[Result - 1] - Result; Result := Result + FOffsetAtStart; end; constructor TSynWordWrapLineMap.Create; begin FInvalidLines.Create; inherited Create; end; destructor TSynWordWrapLineMap.Destroy; begin FInvalidLines.Destroy; inherited Destroy; end; function TSynWordWrapLineMap.GetDumpData: String; begin Result := format('Offs=%3d Rlcnt=%4d VCnt=%4d Diff=%3d Inval=%3d..%3d // ', [FOffsetAtStart, RealCount, ViewedCount, ViewedRealCountDifference, GetFirstInvalidLine, GetLastInvalidLine]); if FWrappedExtraSumsCount = 1 then Result := Result + IntToStr(FWrappedExtraSums[0]) else if FWrappedExtraSumsCount = 2 then Result := Result + IntToStr(FWrappedExtraSums[0]) + ', ' + IntToStr(FWrappedExtraSums[1]) else if FWrappedExtraSumsCount > 2 then Result := Result + IntToStr(FWrappedExtraSums[0]) + ' / ' + IntToStr(FWrappedExtraSums[FWrappedExtraSumsCount-2]) + ', ' + IntToStr(FWrappedExtraSums[FWrappedExtraSumsCount-1]) end; function TSynWordWrapLineMap.GetFirstInvalidLine: Integer; begin Result := FInvalidLines.FirstInvalidLine; end; function TSynWordWrapLineMap.GetFirstInvalidEndLine: Integer; begin Result := FInvalidLines.FirstInvalidEndLine; end; function TSynWordWrapLineMap.GetLastInvalidLine: Integer; begin Result := FInvalidLines.LastInvalidLine; end; { TSynWordWrapIndexPage } function TSynWordWrapIndexPage.GetFirstInvalidLine: Integer; begin Result := FSynWordWrapLineMap.FirstInvalidLine; end; function TSynWordWrapIndexPage.GetFirstInvalidEndLine: Integer; begin Result := FSynWordWrapLineMap.FirstInvalidEndLine; end; function TSynWordWrapIndexPage.GetLastInvalidLine: Integer; begin Result := FSynWordWrapLineMap.LastInvalidLine; end; function TSynWordWrapIndexPage.GetViewedRealCountDifference: Integer; begin Result := FSynWordWrapLineMap.ViewedRealCountDifference; end; procedure TSynWordWrapIndexPage.UpdateViewedSizeDifference; begin UpdateNodeSize(FSynWordWrapLineMap.ViewedRealCountDifference); end; procedure TSynWordWrapIndexPage.MaybeJoinWithSibling; var dummy, NextLineOffs, PrevLineOffs, NextLineDist, PrevLineDist, c: Integer; NextPage, PrevPage: TSynEditLineMapPage; begin if (FSynWordWrapLineMap.FirstInvalidLine < 0) and (RealCount <= Tree.PageJoinSize) then begin NextLineOffs := 0; dummy := 0; NextPage := Successor(NextLineOffs, dummy); if NextPage <> nil then begin assert(NextLineOffs > RealEndLine, 'TSynWordWrapIndexPage.MaybeJoinWithSibling: NextLineOffs > RealEndLine'); NextLineDist := NextLineOffs - (RealEndLine+1) + NextPage.RealStartLine; c := NextPage.RealCount; if ( (c <> 0) and (NextLineDist > Tree.PageJoinDistance) ) or (c > Tree.PageJoinSize) or (NextPage.FirstInvalidLine >= 0) or (not NextPage.CanExtendStartTo(-NextLineOffs + RealStartLine, True)) then NextLineOffs := 0; end else NextLineOffs := 0; PrevLineOffs := 0; dummy := 0; PrevPage := Precessor(PrevLineOffs, dummy); if PrevPage <> nil then begin PrevLineOffs := -PrevLineOffs; assert(PrevLineOffs > PrevPage.RealEndLine, 'TSynWordWrapIndexPage.MaybeJoinWithSibling: -PrevLineOffs > PrevPage.RealEndLine'); PrevLineDist := PrevLineOffs + RealStartLine - (PrevPage.RealEndLine+1); c := PrevPage.RealCount; if ( (c <> 0) and (PrevLineDist> Tree.PageJoinDistance) ) or (c > Tree.PageJoinSize) or (PrevPage.FirstInvalidLine >= 0) or (not PrevPage.CanExtendEndTo(PrevLineOffs + RealEndLine, True)) then PrevLineOffs := 0; end else PrevLineOffs := 0; if (NextLineOffs > 0) and ( (PrevLineOffs = 0) or (PrevLineDist > NextLineDist) ) then begin MoveLinesAtEndTo(NextPage, 0, NextLineOffs); Tree.FreeNode(Self); NextPage.AdjustPosition(-NextLineOffs); end else if (PrevLineOffs > 0) then begin MoveLinesAtStartTo(PrevPage, RealEndLine, PrevLineOffs); Tree.FreeNode(Self); end; end; end; function TSynWordWrapIndexPage.GetWrappedOffsetFor(ARealOffset: IntIdx): IntIdx; begin Result := FSynWordWrapLineMap.GetWrappedOffsetFor(ARealOffset); end; function TSynWordWrapIndexPage.IsValid: boolean; begin Result := FSynWordWrapLineMap.FInvalidLines.Count = 0; end; function TSynWordWrapIndexPage.CanExtendStartTo(ALineOffs: Integer; AIgnoreJoinDist: Boolean): boolean; begin Result := (RealEndLine - ALineOffs < Tree.PageSplitSize) and (AIgnoreJoinDist or (RealStartLine - ALineOffs < Tree.PageJoinDistance)); end; function TSynWordWrapIndexPage.CanExtendEndTo(ALineOffs: Integer; AIgnoreJoinDist: Boolean): boolean; begin Result := (ALineOffs - RealStartLine < Tree.PageSplitSize) and (AIgnoreJoinDist or (ALineOffs - RealEndLine < Tree.PageJoinDistance)); end; function TSynWordWrapIndexPage.GetOffsetForWrap(AWrapOffset: IntIdx; out ASubOffset: IntIdx): IntIdx; begin Result := FSynWordWrapLineMap.GetOffsetForWrap(AWrapOffset, ASubOffset); end; function TSynWordWrapIndexPage.TextXYIdxToViewXYIdx(ATextXYIdx: TPhysPoint; ANodeStartLine: IntIdx): TPhysPoint; var p: TPoint; begin Result := inherited TextXYIdxToViewXYIdx(ATextXYIdx, ANodeStartLine); p := FSynEditWrappedPlugin.TextXYToLineXY(Result); Result.y := ANodeStartLine + GetWrappedOffsetFor(Result.y - ANodeStartLine); Result.x := p.x; Result.y := Result.y + p.y; end; function TSynWordWrapIndexPage.ViewXYIdxToTextXYIdx(AViewXYIdx: TPhysPoint; ANodeStartLine: IntIdx): TPhysPoint; var SubOffset: Integer; begin Result := inherited ViewXYIdxToTextXYIdx(AViewXYIdx, ANodeStartLine); Result.y := ANodeStartLine + GetOffsetForWrap(Result.y - ANodeStartLine, SubOffset); Result.x := FSynEditWrappedPlugin.LineXYToTextX(Result.y, Point(Result.x, SubOffset) ); end; procedure TSynWordWrapIndexPage.InvalidateLines(AFromOffset, AToOffset: Integer); begin FSynWordWrapLineMap.InvalidateLines(AFromOffset, AToOffset); end; function TSynWordWrapIndexPage.ExtendAndInvalidateLines(AFromLineIdx, AToLineIdx: TLineIdx): Boolean; begin Result := True; if AFromLineIdx < 0 then begin AdjustPosition(AFromLineIdx); InsertLinesAtOffset(0, -AFromLineIdx); AToLineIdx := AToLineIdx - AFromLineIdx; AFromLineIdx := 0; end; InvalidateLines(AFromLineIdx, AToLineIdx); end; constructor TSynWordWrapIndexPage.Create(ATree: TSynLineMapAVLTree); begin FSynWordWrapLineMap := TSynWordWrapLineMap.Create; FSynWordWrapLineMap.FAvlNode := Self; inherited Create(ATree); end; destructor TSynWordWrapIndexPage.Destroy; begin FSynWordWrapLineMap.Destroy; inherited Destroy; end; procedure TSynWordWrapIndexPage.DumpNode(ALine: Integer; AnIndent: Integer); var s: String; begin s:= StringOfChar(' ', AnIndent)+IntToHex(AnIndent,1); ALine := ALine + NodeLineOffset; if Left <> nil then Left.DumpNode(ALine, AnIndent+1); DebugLn('%-10s WRAP LnOffs=%5d LINE=%5d LSzSum==%4d LineCnt=%4d Sz=%3d %s', [ s, NodeLineOffset, ALine, LeftSizeSum, RealCount, FSize, FSynWordWrapLineMap.GetDumpData ]); if Right <> nil then Right.DumpNode(ALine, AnIndent+1); end; procedure TSynWordWrapIndexPage.AdjustForLinesInserted(AStartLine, ALineCount: IntIdx; ABytePos: Integer); var rs, re, LineOffs, dummy, Cnt: Integer; NextPage, PrevPage: TSynEditLineMapPage; begin assert(AStartLine >= 0, 'TSynWordWrapIndexPage.AdjustForLinesInserted: AStartLine >= 0'); rs := RealStartLine; re := RealEndLine; if (AStartLine <= rs) or (AStartLine > re) or (re - rs + 1 + ALineCount <= Tree.PageSplitSize) then begin InsertLinesAtOffset(AStartLine, ALineCount); if AStartLine = 0 then AdjustPosition(-ALineCount); exit; end; (* This node was NOT moved by the callers AdjustForLinesInserted. This would only have happened if AStartLine = 0 *) assert(RealCount > 0, 'TSynWordWrapIndexPage.InsertLines: RealCount > 0'); if AStartLine > rs + (re-rs) div 2 then begin // try split to next LineOffs := 0; dummy := 0; NextPage := Successor(LineOffs, dummy); if (NextPage<>nil) and NextPage.CanExtendStartTo(AStartLine + ALineCount - LineOffs) then begin //CurrentPage.SplitNodeToNext(NextPage, AStartLine); Cnt := LineOffs - (AStartLine + ALineCount); MoveLinesAtEndTo(NextPage, AStartLine, Cnt); NextPage.AdjustPosition(-Cnt); //CurrentPage.InsertLinesAtIndex(AStartLine, ACount); InsertLinesAtOffset(AStartLine, ALineCount); exit; end; LineOffs := 0; dummy := 0; PrevPage := Precessor(LineOffs, dummy); if (PrevPage<>nil) and PrevPage.CanExtendEndTo(AStartLine - 1 - LineOffs) then begin //CurrentPage.SplitNodeToPrev(PrevPage, AStartLine - 1); MoveLinesAtStartTo(PrevPage, AStartLine - 1, -LineOffs); AdjustPosition(AStartLine + ALineCount); //PrevPage.InsertLinesAtIndex(AStartLine, ACount); PrevPage.InsertLinesAtOffset(-LineOffs + AStartLine, ALineCount); exit; end; //CurrentPage.SplitNodeToNewNext(NextPage, AStartLine); NextPage := Tree.FindPageForLine(GetPosition + AStartLine + ALineCount, afmCreate).Page; MoveLinesAtEndTo(NextPage, AStartLine, Max(LastInvalidLine, RealCount)); // May be bigger than needed.... //CurrentPage.InsertLinesAtIndex(AStartLine, ACount); InsertLinesAtOffset(AStartLine, ALineCount); end else begin // try split to prev LineOffs := 0; dummy := 0; PrevPage := Precessor(LineOffs, dummy); if (PrevPage<>nil) and PrevPage.CanExtendEndTo(AStartLine - 1 - LineOffs) then begin //CurrentPage.SplitNodeToPrev(PrevPage, AStartLine - 1); MoveLinesAtStartTo(PrevPage, AStartLine - 1, -LineOffs); AdjustPosition(AStartLine + ALineCount); //PrevPage.InsertLinesAtIndex(AStartLine, ACount); PrevPage.InsertLinesAtOffset(-LineOffs + AStartLine, ALineCount); exit; end; LineOffs := 0; dummy := 0; NextPage := Successor(LineOffs, dummy); if (NextPage<>nil) and NextPage.CanExtendStartTo(AStartLine + ALineCount - LineOffs) then begin //CurrentPage.SplitNodeToNext(NextPage, AStartLine { + ALineCount}); Cnt := LineOffs - (AStartLine + ALineCount); MoveLinesAtEndTo(NextPage, AStartLine, Cnt); NextPage.AdjustPosition(-Cnt); //CurrentPage.InsertLinesAtIndex(AStartLine, ACount); InsertLinesAtOffset(AStartLine, ALineCount); exit; end; //CurrentPage.SplitNodeToNewPrev(PrevPage, AStartLine - 1); dummy := GetPosition; AdjustPosition(AStartLine + ALineCount); PrevPage := Tree.FindPageForLine(dummy, afmCreate).Page; MoveLinesAtStartTo(PrevPage, AStartLine - 1, 0); //PrevPage.InsertLinesAtIndex(AStartLine, ACount); PrevPage.InsertLinesAtOffset(AStartLine, ALineCount); end; // inherited AdjustForLinesInserted(AStartLine, ALineCount, ABytePos); end; procedure TSynWordWrapIndexPage.AdjustForLinesDeleted(AStartLine, ALineCount: IntIdx; ABytePos: Integer); begin DeleteLinesAtOffset(AStartLine, ALineCount); MaybeJoinWithSibling; end; procedure TSynWordWrapIndexPage.InsertLinesAtOffset(ALineOffset, ALineCount: IntIdx); begin FSynWordWrapLineMap.InsertLinesAtOffset(ALineOffset, ALineCount); end; procedure TSynWordWrapIndexPage.DeleteLinesAtOffset(ALineOffset, ALineCount: IntIdx; ADoNotShrink: Boolean); begin FSynWordWrapLineMap.DeleteLinesAtOffset(ALineOffset, ALineCount, ADoNotShrink); end; procedure TSynWordWrapIndexPage.MoveLinesAtStartTo( ADestPage: TSynEditLineMapPage; ASourceEndLine, ATargetStartLine: Integer); begin // TODO: adestpage <> TSynWordWrapIndexPage assert(ADestPage is TSynWordWrapIndexPage, 'TSynWordWrapIndexPage.MoveLinesAtStartTo: ADestPage is TSynWordWrapIndexPage'); FSynWordWrapLineMap.MoveLinesAtStartTo(TSynWordWrapIndexPage(ADestPage).FSynWordWrapLineMap, ASourceEndLine, ATargetStartLine); FSynWordWrapLineMap.DeleteLinesAtOffset(0, ASourceEndLine + 1); end; procedure TSynWordWrapIndexPage.MoveLinesAtEndTo( ADestPage: TSynEditLineMapPage; ASourceStartLine, ACount: Integer); begin // TODO: adestpage <> TSynWordWrapIndexPage assert(ADestPage is TSynWordWrapIndexPage, 'TSynWordWrapIndexPage.MoveLinesAtEndTo: ADestPage is TSynWordWrapIndexPage'); FSynWordWrapLineMap.MoveLinesAtEndTo(TSynWordWrapIndexPage(ADestPage).FSynWordWrapLineMap, ASourceStartLine, ACount); FSynWordWrapLineMap.DeleteLinesAtOffset(ASourceStartLine, ACount); end; procedure TSynWordWrapIndexPage.EndValidate; begin FSynWordWrapLineMap.EndValidate; MaybeJoinWithSibling; end; procedure TSynWordWrapIndexPage.ValidateLine(ALineOffset, AWrappCount: Integer); begin FSynWordWrapLineMap.ValidateLine(ALineOffset, AWrappCount); end; function TSynWordWrapIndexPage.RealCount: Integer; begin Result := FSynWordWrapLineMap.RealCount; end; function TSynWordWrapIndexPage.RealStartLine: Integer; begin Result := FSynWordWrapLineMap.Offset; end; function TSynWordWrapIndexPage.RealEndLine: Integer; begin Result := FSynWordWrapLineMap.Offset + FSynWordWrapLineMap.RealCount - 1; end; { TLazSynDisplayWordWrap } constructor TLazSynDisplayWordWrap.Create(AWrappedView: TSynEditLineMappingView; AWrapPlugin: TLazSynEditLineWrapPlugin); begin FWrapPlugin := AWrapPlugin; inherited Create(AWrappedView); end; procedure TLazSynDisplayWordWrap.SetHighlighterTokensLine( AWrappedLine: TLineIdx; out ARealLine: TLineIdx; out AStartBytePos, ALineByteLen: Integer); var IsNext: Boolean; PrevSub: IntIdx; LineTxt: String; PWidth: TPhysicalCharWidths; PhysWidth, MaxW: Integer; begin IsNext := (AWrappedLine = FCurWrappedLine + 1) and (FCurWrappedLine >= 0); PrevSub := FCurrentWrapSubline; inherited SetHighlighterTokensLine(AWrappedLine, ARealLine, AStartBytePos, ALineByteLen); LineTxt := FLineMappingView.NextLines.Strings[ARealLine]; FLineMappingView.LogPhysConvertor.CurrentLine := ARealLine; PWidth := FLineMappingView.LogPhysConvertor.CurrentWidthsDirect; //PWidth := FLineMappingView.GetPhysicalCharWidths(ARealLine); MaxW := FWrapPlugin.WrapColumn; if IsNext and (FCurrentWrapSubline = PrevSub + 1) then begin FCurSubLineLogStartIdx := FCurSubLineNextLogStartIdx; FCurSubLineNextLogStartIdx := FWrapPlugin.CalculateNextBreak(PChar(LineTxt), FCurSubLineNextLogStartIdx, MaxW, PWidth, PhysWidth); FCurSubLinePhysStartIdx := FCurSubLinePhysStartIdx + PhysWidth; end else begin FWrapPlugin.GetSublineBounds(LineTxt, MaxW, PWidth, FCurrentWrapSubline, FCurSubLineLogStartIdx, FCurSubLineNextLogStartIdx, FCurSubLinePhysStartIdx, PhysWidth); end; AStartBytePos := AStartBytePos + FCurSubLineLogStartIdx; ALineByteLen := FCurSubLineNextLogStartIdx - FCurSubLineLogStartIdx; FCurLineLogIdx := 0; end; function TLazSynDisplayWordWrap.GetNextHighlighterToken(out ATokenInfo: TLazSynDisplayTokenInfo): Boolean; var PreStart: Integer; begin If FCurLineLogIdx >= FCurSubLineNextLogStartIdx then begin Result := False; exit; end; repeat PreStart := FCurSubLineLogStartIdx - FCurLineLogIdx; Result := inherited GetNextHighlighterToken(ATokenInfo); if (not Result) or (ATokenInfo.TokenLength <= 0) then begin exit; end; FCurToken := ATokenInfo; FCurLineLogIdx := FCurLineLogIdx + ATokenInfo.TokenLength; until FCurLineLogIdx > FCurSubLineLogStartIdx; if PreStart > 0 then begin ATokenInfo.TokenStart := ATokenInfo.TokenStart + PreStart; ATokenInfo.TokenLength := ATokenInfo.TokenLength - PreStart; Result := ATokenInfo.TokenLength > 0; if not Result then exit; end; If FCurLineLogIdx > FCurSubLineNextLogStartIdx then begin ATokenInfo.TokenLength := ATokenInfo.TokenLength - (FCurLineLogIdx - FCurSubLineNextLogStartIdx); Result := ATokenInfo.TokenLength > 0; end; end; { TLazSynEditLineWrapPlugin } procedure TLazSynEditLineWrapPlugin.DoLinesChanged(Sender: TObject); begin ValidateAll; end; procedure TLazSynEditLineWrapPlugin.DoWidthChanged(Sender: TObject; Changes: TSynStatusChanges); begin FLineMapView.KnownLengthOfLongestLine := WrapColumn; FLineMapView.InvalidateLines(0, FLineMapView.NextLines.Count); end; function TLazSynEditLineWrapPlugin.GetWrapColumn: Integer; begin Result := TSynEdit(Editor).CharsInWindow; end; function TLazSynEditLineWrapPlugin.CreatePageMapNode(AMapTree: TSynLineMapAVLTree): TSynEditLineMapPage; begin Result := TSynWordWrapIndexPage.Create(AMapTree); TSynWordWrapIndexPage(Result).FSynEditWrappedPlugin := Self; end; procedure TLazSynEditLineWrapPlugin.SetEditor(const AValue: TCustomSynEdit); begin if (Editor <> nil) and (AValue <> nil) then raise Exception.Create('Not allowed to change editor'); inherited SetEditor(AValue); end; function TLazSynEditLineWrapPlugin.CalculateNextBreak(ALine: PChar; ALogStartFrom: IntIdx; AMaxWidth: Integer; const PhysCharWidths: TPhysicalCharWidths; out APhysWidth: Integer): IntIdx; const // todo, other break chars // utf8 BREAKCHARS = [#9, #32, '.', ',', ':', ';', '=', '-', '+', '*', '/', '(', ')', '{', '}', '[', ']', '!', '<', '>']; var PhysWidthPtr: PByte; CurCharPhysWidth: Cardinal; LastGoodPos: PChar; begin if (ALine = nil) or (ALine^ = #0) then exit(0); PhysWidthPtr := @PhysCharWidths[ALogStartFrom]; APhysWidth := AMaxWidth; Result := ALogStartFrom; ALine := ALine + ALogStartFrom; LastGoodPos := ALine; while ALine <> nil do begin if ALine^ in BREAKCHARS then while ALine^ in BREAKCHARS do begin CurCharPhysWidth := PhysWidthPtr^ and PCWMask; if CurCharPhysWidth <= AMaxWidth then begin inc(ALine); inc(PhysWidthPtr); inc(Result); dec(AMaxWidth, CurCharPhysWidth); end else begin ALine := nil; // break outer loop break; end; end else begin CurCharPhysWidth := 0; LastGoodPos := ALine; while (ALine^ <> #0) and not (ALine^ in BREAKCHARS) do begin CurCharPhysWidth := CurCharPhysWidth + PhysWidthPtr^ and PCWMask; inc(ALine); inc(PhysWidthPtr); end; if (CurCharPhysWidth > 0) and (CurCharPhysWidth <= AMaxWidth) then begin inc(Result, ALine-LastGoodPos); dec(AMaxWidth, CurCharPhysWidth); end else begin ALine := nil; // break outer loop break; end; end; end; if Result = ALogStartFrom then begin PhysWidthPtr := @PhysCharWidths[0]; ALine := LastGoodPos; while ALine^ <> #0 do begin CurCharPhysWidth := PhysWidthPtr^ and PCWMask; if (CurCharPhysWidth <= AMaxWidth) or (Result = ALogStartFrom) then begin inc(ALine); inc(PhysWidthPtr); inc(Result); dec(AMaxWidth, CurCharPhysWidth); end else break; end; end; APhysWidth := APhysWidth - AMaxWidth; end; function TLazSynEditLineWrapPlugin.GetSublineCount(ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths): Integer; var x, dummy: Integer; begin Result := 1; if Length(ALine) = 0 then exit; x := CalculateNextBreak(PChar(ALine), 0, AMaxWidth, APhysCharWidths, dummy); while (x < Length(ALine)) do begin inc(Result); x := CalculateNextBreak(PChar(ALine), x, AMaxWidth, APhysCharWidths, dummy); end; end; procedure TLazSynEditLineWrapPlugin.GetSublineBounds(ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths; ASubLine: Integer; out ALogStartX, ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer); begin ALogStartX := 0; ANextLogStartX := 0; APhysStart := 0; if Length(ALine) = 0 then exit; ANextLogStartX := CalculateNextBreak(PChar(ALine), ALogStartX, AMaxWidth, APhysCharWidths, APhysWidth); while ASubLine > 0 do begin ALogStartX := ANextLogStartX; APhysStart := APhysStart + APhysWidth; ANextLogStartX := CalculateNextBreak(PChar(ALine), ALogStartX, AMaxWidth, APhysCharWidths, APhysWidth); dec(ASubLine); end; end; function TLazSynEditLineWrapPlugin.GetSubLineFromX(ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths; var APhysXPos: Integer): integer; var x, PhysWidth: Integer; begin Result := 0; if Length(ALine) = 0 then exit; Result := -1; x := 0; APhysXPos := ToIdx(APhysXPos); while (x < Length(ALine)) do begin inc(Result); x := CalculateNextBreak(PChar(ALine), x, AMaxWidth, APhysCharWidths, PhysWidth); if x >= Length(ALine) then break; if (FCaretWrapPos = wcpBOL) and (PhysWidth = APhysXPos) and (x < Length(ALine)) then begin inc(Result); APhysXPos := APhysXPos - PhysWidth; break; end; if PhysWidth >= APhysXPos then break; APhysXPos := APhysXPos - PhysWidth; end; APhysXPos := ToPos(APhysXPos); end; procedure TLazSynEditLineWrapPlugin.GetWrapInfoForViewedXY( var AViewedXY: TPhysPoint; AFlags: TViewedXYInfoFlags; out AFirstViewedX: IntPos; ALogPhysConvertor: TSynLogicalPhysicalConvertor); var SubLineOffset, YIdx: TLineIdx; LineTxt: String; PWidth: TPhysicalCharWidths; LogX, NextLogX, PhysX: IntIdx; PhysWidth: Integer; begin YIdx := FLineMapView.Tree.GetLineForForWrap(ToIdx(AViewedXY.y), SubLineOffset); YIdx := FLineMapView.NextLines.ViewToTextIndex(YIdx); LineTxt := FLineMapView.Strings[YIdx]; ALogPhysConvertor.CurrentLine := YIdx; PWidth := ALogPhysConvertor.CurrentWidthsDirect; GetSublineBounds(LineTxt, WrapColumn, PWidth, SubLineOffset, LogX, NextLogX, PhysX, PhysWidth); case CaretWrapPos of wcpEOL: begin if (SubLineOffset > 0) and (AViewedXY.x <= 1) then AViewedXY.x := 2 else if (NextLogX < length(LineTxt)) and (AViewedXY.x > ToPos(PhysWidth)) then AViewedXY.x := ToPos(PhysWidth); AFirstViewedX := 2; end; wcpBOL: begin if (NextLogX < length(LineTxt)) and (AViewedXY.x >= ToPos(PhysWidth)) then AViewedXY.x := ToPos(PhysWidth) - 1; AFirstViewedX := 1; end; end; AViewedXY.y := ToPos(YIdx); AViewedXY.x := AViewedXY.x + PhysX; end; function TLazSynEditLineWrapPlugin.TextXYToLineXY(ATextXY: TPhysPoint ): TPhysPoint; begin FLineMapView.LogPhysConvertor.CurrentLine := ATextXY.y; Result.x := ATextXY.x; Result.y := GetSubLineFromX(FLineMapView.NextLines.Strings[ATextXY.y], WrapColumn, FLineMapView.LogPhysConvertor.CurrentWidthsDirect, Result.x ); end; function TLazSynEditLineWrapPlugin.LineXYToTextX(ARealLine: IntPos; ALineXY: TPhysPoint): Integer; var dummy, dummy2: IntIdx; dummy3: integer; begin FLineMapView.LogPhysConvertor.CurrentLine := ARealLine; GetSublineBounds(FLineMapView.NextLines.Strings[ARealLine], WrapColumn, FLineMapView.LogPhysConvertor.CurrentWidthsDirect, ALineXY.y, dummy, dummy2, Result, dummy3 ); Result := Result + ALineXY.x; end; function TLazSynEditLineWrapPlugin.CalculateWrapForLine(ALineIdx: IntIdx; AMaxWidth: integer): Integer; begin FLineMapView.LogPhysConvertor.CurrentLine := ALineIdx; Result := GetSublineCount(FLineMapView.NextLines.Strings[ALineIdx], AMaxWidth, FLineMapView.LogPhysConvertor.CurrentWidthsDirect); end; constructor TLazSynEditLineWrapPlugin.Create(AOwner: TComponent); begin inherited Create(AOwner); FLineMapView := TSynEditLineMappingView(TSynEdit(Editor).TextViewsManager.SynTextViewByClass[TSynEditLineMappingView]); if FLineMapView = nil then begin FLineMapView := TSynEditLineMappingView.Create; TSynEdit(Editor).TextViewsManager.AddTextView(FLineMapView); end else if (not(FLineMapView.DisplayView is TLazSynDisplayLineMapping)) or (FLineMapView.PageMapCreator <> nil) then raise Exception.Create('Conflicting Plugin detected'); FLineMapView.SetDisplayView(TLazSynDisplayWordWrap.Create(FLineMapView, Self)); FLineMapView.PageMapCreator := @CreatePageMapNode; FLineMapView.WrapInfoForViewedXYProc := @GetWrapInfoForViewedXY; FLineMapView.AddLinesChangedHandler(@DoLinesChanged); TSynEdit(Editor).RegisterStatusChangedHandler(@DoWidthChanged, [scCharsInWindow]); FLineMapView.KnownLengthOfLongestLine := WrapColumn; WrapAll; end; procedure TLazSynEditLineWrapPlugin.WrapAll; var c: Integer; begin FLineMapView.Tree.Clear; c := FLineMapView.NextLines.Count; if c > 0 then FLineMapView.Tree.AdjustForLinesInserted(0, c, 0); ValidateAll; end; procedure TLazSynEditLineWrapPlugin.ValidateAll; var AMaxWidth, i, w: Integer; LowLine, HighLine: TLineIdx; begin if not FLineMapView.Tree.NeedsValidation then exit; AMaxWidth := WrapColumn; while FLineMapView.Tree.NextBlockForValidation(LowLine, HighLine) do begin for i := LowLine to HighLine do begin w := CalculateWrapForLine(i, AMaxWidth); FLineMapView.Tree.ValidateLine(i, w); end; end; FLineMapView.Tree.EndValidate; FLineMapView.SendNotification(senrLineMappingChanged, FLineMapView, 0, 0); TSynEdit(Editor).Invalidate; end; end.