{ /*************************************************************************** textstrings.pas --------------- Component Library Code ***************************************************************************/ ***************************************************************************** This file is part of the Lazarus Component Library (LCL) See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** TTextStrings is a TStrings descendent that is optimized for handling the complete text as whole (instead of as line by line as in TStringList). TCustomMemoStrings is a TStrings descendent which works around the behavior of TMemo.Lines, which contains the text with wordwrap line endings, in order to store the text in the LFM without those wordwrap line endings. See bug 30659 } unit TextStrings; {$mode objfpc}{$H+} interface uses Classes, SysUtils, // LazUtils LazUtf8Classes, LazUtilsStrConsts; type { TTextStrings } TTextLineRange = record Line: string; // cached line as string TheObject: TObject; // user data StartPos: integer; // start of line in Text EndPos: integer; // end of line in Text (= start of newline character(s)) end; PTextLineRange = ^TTextLineRange; TCustomMemoStrings = class(TStrings) protected procedure DoReadData(Reader: TReader); virtual; procedure DoWriteData(Writer: TWriter); virtual; procedure DefineProperties(Filer: TFiler); override; end; TTextStrings = class(TCustomMemoStrings) private FOnChange: TNotifyEvent; FOnChanging: TNotifyEvent; protected FArraysValid: boolean; FLineCount: integer; FLineCapacity: integer; FLineRanges: PTextLineRange;// array of TTextLineRange FText: string; FUpdateCount: integer; FChangedWhileUpdate: boolean; function GetTextStr: string; override; procedure SetTextStr(const AValue: string); override; procedure BuildArrays; virtual; function GetCount: Integer; override; procedure Changed; virtual; procedure Changing; virtual; function Get(Index: Integer): string; override; procedure ClearArrays; function GetObject(Index: Integer): TObject; override; procedure Put(Index: Integer; const S: string); override; procedure PutObject(Index: Integer; AnObject: TObject); override; function GetLineLen(Index: integer; IncludeNewLineChars: boolean): integer; inline; function GetLineEnd(Index: integer; IncludeNewLineChars: boolean): integer; function CountLineEndings(const s: string): integer; public constructor Create; destructor Destroy; override; procedure Clear; override; procedure SetText(TheText: PChar); override; procedure Insert(Index: Integer; const S: string); override; procedure Delete(Index: Integer); override; procedure Exchange(Index1, Index2: Integer); override; procedure Move(CurIndex, NewIndex: Integer); override; procedure MakeTextBufferUnique; procedure BeginUpdate; procedure EndUpdate; function GetText: PChar; override; function IndexOf(const S: string): Integer; override; function Add(const S: string): Integer; override; function AddObject(const S: string; AObject: TObject): Integer; override; procedure AddStrings(TheStrings: TStrings); override; procedure LoadFromFile(const FileName: string); override; procedure SaveToFile(const FileName: string); override; public property Text: string read FText write SetTextStr; property OnChange: TNotifyEvent read FOnChange write FOnChange; property OnChanging: TNotifyEvent read FOnChanging write FOnChanging; end; implementation { TCustomMemoStrings } procedure TCustomMemoStrings.DoReadData(Reader: TReader); begin Reader.ReadListBegin; BeginUpdate; try Clear; while not Reader.EndOfList do Add(Reader.ReadString); finally EndUpdate; end; Reader.ReadListEnd; end; procedure TCustomMemoStrings.DoWriteData(Writer: TWriter); var i: Integer; lStringsNoWordWrap: TStringList; begin lStringsNoWordWrap := TStringList.Create; try lStringsNoWordWrap.Text := Text; Writer.WriteListBegin; for i := 0 to lStringsNoWordWrap.Count - 1 do Writer.WriteString(lStringsNoWordWrap.Strings[i]); Writer.WriteListEnd; finally lStringsNoWordWrap.Free; end; end; procedure TCustomMemoStrings.DefineProperties(Filer: TFiler); var HasData: Boolean; begin HasData := Count > 0; Filer.DefineProperty('Strings', @DoReadData, @DoWriteData, HasData); end; { TTextStrings } function TTextStrings.GetTextStr: string; begin Result:=FText; end; procedure TTextStrings.SetTextStr(const AValue: string); begin if FText=AValue then exit; FText:=AValue; FArraysValid:=false; end; procedure TTextStrings.BuildArrays; var p, line: integer; l: Integer; ArraySize: Integer; begin if FArraysValid then exit; ClearArrays; FArraysValid:=true; // count line ends FLineCount:=CountLineEndings(FText); l:=length(FText); if (FText<>'') and (not (FText[l] in [#10,#13])) then inc(FLineCount); FLineCapacity:=FLineCount; // build line range list if FLineCount>0 then begin ArraySize:=FLineCount*SizeOf(TTextLineRange); GetMem(FLineRanges,ArraySize); FillByte(FLineRanges^,ArraySize,0); p:=1; line:=0; FLineRanges[line].StartPos:=1; FLineRanges[FLineCount-1].EndPos:=l+1; while (p<=l) do begin if (not (FText[p] in [#10,#13])) then begin inc(p); end else begin // new line FLineRanges[line].EndPos:=p; inc(line); inc(p); if (p<=l) and (FText[p] in [#10,#13]) and (FText[p]<>FText[p-1]) then inc(p); if line0) then begin FChangedWhileUpdate:=true; exit; end; if Assigned(FOnChange) then FOnChange(Self); end; procedure TTextStrings.Changing; begin if FUpdateCount=0 then if Assigned(FOnChanging) then FOnChanging(Self); end; function TTextStrings.Get(Index: Integer): string; var Line: PTextLineRange; begin if not FArraysValid then BuildArrays; if (Index<0) or (Index>=FLineCount) then Error(lrsListIndexExceedsBounds, Index); Line:=@FLineRanges[Index]; if (Line^.Line='') and (Line^.StartPosnil then begin for i:=0 to FLineCount-1 do FLineRanges[i].Line:=''; FreeMem(FLineRanges); FLineRanges:=nil; end; FLineCapacity:=0; end; function TTextStrings.GetObject(Index: Integer): TObject; begin if FArraysValid then begin if (Index<0) or (Index>=FLineCount) then Error(lrsListIndexExceedsBounds, Index); Result:=FLineRanges[Index].TheObject; end else Result:=nil; end; procedure TTextStrings.Put(Index: Integer; const S: string); var OldLineLen: Integer; NewLineLen: Integer; Movement: Integer; OldStartPos: LongInt; OldEndPos: LongInt; MoveLen: Integer; i: Integer; NewEndPos: Integer; begin if not FArraysValid then BuildArrays; if (Index<0) or (Index>=FLineCount) then Error(lrsListIndexExceedsBounds, Index); OldStartPos:=FLineRanges[Index].StartPos; OldEndPos:=FLineRanges[Index].EndPos; NewLineLen:=length(s); OldLineLen:=OldEndPos-OldStartPos; Movement:=NewLineLen-OldLineLen; NewEndPos:=OldEndPos+Movement; // move text behind MoveLen := Length(FText) - OldEndPos + 1; if (Movement<>0) and (MoveLen>0) then begin if Movement > 0 then SetLength(FText, Length(FText) + Movement); System.Move(FText[OldEndPos], FText[NewEndPos], MoveLen); if Movement < 0 then SetLength(FText, Length(FText) + Movement); for i := Index + 1 to FLineCount - 1 do begin inc(FLineRanges[i].StartPos, Movement); inc(FLineRanges[i].EndPos, Movement); end; end; FLineRanges[Index].EndPos:=NewEndPos; // copy text if NewLineLen>0 then System.Move(S[1],FText[OldStartPos],NewLineLen); FLineRanges[Index].Line:=S; // check if arrays need rebuild i:=NewLineLen; while (i>0) and (not (S[i] in [#10,#13])) do dec(i); if i>0 then begin // S contains new line chars => rebuild needed FArraysValid:=false; end; end; procedure TTextStrings.PutObject(Index: Integer; AnObject: TObject); begin if not FArraysValid then BuildArrays; if (Index<0) or (Index>=FLineCount) then Error(lrsListIndexExceedsBounds, Index); FLineRanges[Index].TheObject:=AnObject; end; function TTextStrings.GetLineLen(Index: integer; IncludeNewLineChars: boolean ): integer; begin Result:=GetLineEnd(Index,IncludeNewLineChars)-FLineRanges[Index].StartPos; end; function TTextStrings.GetLineEnd(Index: integer; IncludeNewLineChars: boolean ): integer; begin if not FArraysValid then BuildArrays; if not IncludeNewLineChars then Result:=FLineRanges[Index].EndPos else if Index=FLineCount-1 then Result:=length(FText)+1 else Result:=FLineRanges[Index+1].StartPos; end; function TTextStrings.CountLineEndings(const s: string): integer; var p: Integer; l: Integer; begin Result:=0; l:=length(s); p:=1; while p<=l do begin if s[p] in [#10,#13] then begin inc(Result); inc(p); if (p<=l) and (s[p] in [#10,#13]) and (s[p-1]<>s[p]) then inc(p); end else begin inc(p); end; end; end; constructor TTextStrings.Create; begin inherited Create; CheckSpecialChars; end; destructor TTextStrings.Destroy; begin Clear; inherited Destroy; end; procedure TTextStrings.Clear; begin ClearArrays; FLineCount:=0; FText:=''; end; procedure TTextStrings.SetText(TheText: PChar); begin if FText=TheText then exit; FText:=TheText; FArraysValid:=false; end; procedure TTextStrings.Insert(Index: Integer; const S: string); procedure RaiseOutOfBounds; begin raise EListError.Create('insert index '+IntToStr(Index)+' out of bounds '+IntToStr(FLineCount)); end; var NewStartPos: Integer; NewLineCharCount: Integer; NewLineLen: Integer; i: Integer; SEndsInNewLine: boolean; Range: PTextLineRange; NewCapacity: Integer; begin if not FArraysValid then BuildArrays; NewLineLen:=length(S); SEndsInNewLine:=(S<>'') and (S[NewLineLen] in [#10,#13]); if IndexFLineCount then RaiseOutOfBounds; NewStartPos:=length(FText)+1; end; NewLineCharCount:=0; if SEndsInNewLine then begin inc(NewLineCharCount); if (NewLineLen>1) and (S[NewLineLen-1] in [#10,#13]) and (S[NewLineLen-1]<>S[NewLineLen]) then inc(NewLineCharCount); System.Insert(S,FText,NewStartPos); end else begin // append missing newline char System.Insert(S+LineEnding,FText,NewStartPos); NewLineCharCount:=length(LineEnding); inc(NewLineLen,NewLineCharCount); end; // adjust arrays if FLineCount=FLineCapacity then begin if FLineCapacity<8 then NewCapacity:=8 else NewCapacity:=FLineCapacity shl 1; ReAllocMem(FLineRanges,SizeOf(TTextLineRange)*NewCapacity); FillByte(FLineRanges[FLineCapacity],SizeOf(TTextLineRange)*(NewCapacity-FLineCapacity),0); FLineCapacity:=NewCapacity; end; if Index=FLineCount) then Error(lrsListIndexExceedsBounds, Index); // adjust text OldLineLen:=GetLineLen(Index,true); if OldLineLen>0 then begin OldStartPos:=FLineRanges[Index].StartPos; System.Delete(FText,OldStartPos,OldLineLen); end; // adjust arrays dec(FLineCount); FLineRanges[Index].Line:=''; if Index=FLineCount then Error(lrsListIndexExceedsBounds, Index1); if Index2>=FLineCount then Error(lrsListIndexExceedsBounds, Index2); // make sure Index1Index2 then begin Dummy:=Index1; Index1:=Index2; Index2:=Dummy; end; Line1:=@FLineRanges[Index1]; Line2:=@FLineRanges[Index2]; // adjust text MakeTextBufferUnique; if (Index2=FLineCount-1) and (Line2^.EndPos>length(FText)) then begin // The last line should be exchanged, // but Text has no new line character(s) at the end // => add LineEnding FText:=FText+LineEnding; end; // get line lengths including new line chars LineLen1:=GetLineLen(Index1,true); LineLen2:=GetLineLen(Index2,true); if (LineLen1<1) and (LineLen2<1) then exit; LineShortLen1:=GetLineLen(Index1,false); LineShortLen2:=GetLineLen(Index2,false); // save the bigger line StartPos1:=Line1^.StartPos; StartPos2:=Line2^.StartPos; if LineLen1>=LineLen2 then begin GetMem(buf,LineLen1); System.Move(FText[StartPos1],buf^,LineLen1); end else begin GetMem(buf,LineLen2); System.Move(FText[StartPos2],buf^,LineLen2); end; // move text in between OldBetweenStart:=StartPos1+LineLen1; BetweenLength:=StartPos2-OldBetweenStart; NewBetweenStart:=StartPos1+LineLen2; Movement:=NewBetweenStart-OldBetweenStart; if (BetweenLength>0) and (Movement<>0) then System.Move(FText[OldBetweenStart],FText[NewBetweenStart],BetweenLength); // move both lines Line1^.Line:=''; Line2^.Line:=''; if LineLen1>=LineLen2 then begin System.Move(FText[StartPos2],FText[StartPos1],LineLen2); System.Move(buf^,FText[StartPos2+Movement],LineLen1); end else begin System.Move(FText[StartPos1],FText[StartPos2+Movement],LineLen1); System.Move(buf^,FText[StartPos1],LineLen2); end; // adjust line ranges if Movement<>0 then begin Line1^.EndPos:=Line1^.StartPos+LineShortLen2; inc(Line2^.StartPos,Movement); Line2^.EndPos:=Line2^.StartPos+LineShortLen1; for i:=Index1+1 to Index2-1 do begin inc(FLineRanges[i].StartPos,Movement); inc(FLineRanges[i].EndPos,Movement); end; end; // exchange TheObject Obj:=Line1^.TheObject; Line1^.TheObject:=Line2^.TheObject; Line2^.TheObject:=Obj; // clean up FreeMem(buf); end; procedure TTextStrings.Move(CurIndex, NewIndex: Integer); var SrcPos1: LongInt; SrcPos2: LongInt; SrcPos3: LongInt; LineStr: String; LineLen: Integer; i: LongInt; Obj: TObject; LineShortLen: LongInt; Line: PTextLineRange; begin // check values if CurIndex=NewIndex then exit; if CurIndex<0 then Error(lrsListIndexExceedsBounds, CurIndex); if NewIndex<0 then Error(lrsListIndexExceedsBounds, NewIndex); if not FArraysValid then BuildArrays; if CurIndex>=FLineCount then Error(lrsListIndexExceedsBounds, CurIndex); if NewIndex>=FLineCount then Error(lrsListIndexExceedsBounds, NewIndex); // adjust text MakeTextBufferUnique; if CurIndexlength(FText)) then begin // CurIndex should be moved to the end, // but Text has no new line character(s) at the end // => add LineEnding FText:=FText+LineEnding; end; SrcPos1:=FLineRanges[CurIndex].StartPos; SrcPos2:=FLineRanges[CurIndex+1].StartPos; SrcPos3:=GetLineEnd(NewIndex,true); // store current line with line end LineLen:=SrcPos2-SrcPos1; LineShortLen:=GetLineLen(CurIndex,false); LineStr:=copy(FText,SrcPos1,LineLen); Obj:=FLineRanges[CurIndex].TheObject; // move lines -1 System.Move(FText[SrcPos2],FText[SrcPos1],SrcPos3-SrcPos2); for i:=CurIndex+1 to NewIndex do begin dec(FLineRanges[i].StartPos,LineLen); dec(FLineRanges[i].EndPos,LineLen); end; System.Move(FLineRanges[CurIndex+1],FLineRanges[CurIndex], SizeOf(TTextLineRange)*(NewIndex-CurIndex)); // put current line at new position i:=SrcPos3-LineLen; System.Move(LineStr[1],FText[i],LineLen); Line:=@FLineRanges[NewIndex]; Line^.StartPos:=i; Line^.EndPos:=i+LineShortLen; Pointer(Line^.Line):=nil; // this will be updated on demand, see Get Line^.TheObject:=Obj; end else begin // move to lower index if (CurIndex=FLineCount-1) and (FLineRanges[CurIndex].EndPos>length(FText)) then begin // CurIndex should be moved from the end, // but Text has no new line character(s) at the end // => add LineEnding FText:=FText+LineEnding; end; SrcPos1:=FLineRanges[NewIndex].StartPos; SrcPos2:=FLineRanges[CurIndex].StartPos; SrcPos3:=GetLineEnd(CurIndex,true); // store current line with line end LineLen:=SrcPos3-SrcPos2; LineShortLen:=GetLineLen(CurIndex,false); LineStr:=copy(FText,SrcPos2,LineLen); Obj:=FLineRanges[CurIndex].TheObject; // move lines +1 System.Move(FText[SrcPos1],FText[SrcPos1+LineLen],SrcPos2-SrcPos1); for i:=CurIndex-1 downto NewIndex do begin inc(FLineRanges[i].StartPos,LineLen); inc(FLineRanges[i].EndPos,LineLen); end; System.Move(FLineRanges[NewIndex],FLineRanges[NewIndex+1], SizeOf(TTextLineRange)*(CurIndex-NewIndex)); // put current line at new position System.Move(LineStr[1],FText[SrcPos1],LineLen); Line:=@FLineRanges[NewIndex]; Line^.StartPos:=SrcPos1; Line^.EndPos:=SrcPos1+LineShortLen; Pointer(Line^.Line):=nil; // this will be updated on demand, see Get Line^.TheObject:=Obj; end; end; procedure TTextStrings.MakeTextBufferUnique; begin // make string unique (refcount=1) to be able to edit it directly UniqueString(FText); end; procedure TTextStrings.BeginUpdate; begin inc(FUpdateCount); end; procedure TTextStrings.EndUpdate; procedure RaiseUpdateCount; begin raise Exception.Create('TTextStrings.EndUpdate'); end; begin if FUpdateCount<=0 then RaiseUpdateCount; dec(FUpdateCount); if FUpdateCount=0 then begin if FChangedWhileUpdate then Changed; end; end; function TTextStrings.GetText: PChar; begin Result:=PChar(FText); end; function TTextStrings.IndexOf(const S: string): Integer; begin Result:=inherited IndexOf(S); end; function TTextStrings.Add(const S: string): Integer; begin Result:=AddObject(S,nil); end; function TTextStrings.AddObject(const S: string; AObject: TObject): Integer; var e: String; NewLineCount: Integer; OldTxtLen: Integer; p: Integer; l: Integer; begin Result:=Count; if (FText<>'') and (not (FText[length(FText)] in [#10,#13])) then e:=LineEnding else e:=''; OldTxtLen:=length(FText); FText:=Text+e+S+LineEnding; if AObject<>nil then BuildArrays; if FArraysValid then begin // update FLineRanges NewLineCount:=FLineCount+CountLineEndings(S)+1; if NewLineCount>FLineCapacity then begin FLineCapacity:=FLineCapacity*2+10; if FLineCapacityFText[p-1]) then inc(p); inc(FLineCount); end; end; end; procedure TTextStrings.AddStrings(TheStrings: TStrings); var s: String; i: Integer; AddEachLine: Boolean; SrcTextStrings: TTextStrings; SrcItem: PTextLineRange; DstItem: PTextLineRange; begin if TheStrings.Count=0 then exit; if FLineCount=0 then begin if TheStrings is TTextStrings then begin // copy Text, lineranges SrcTextStrings:=TTextStrings(TheStrings); FText:=SrcTextStrings.Text; ClearArrays; if not SrcTextStrings.FArraysValid then exit; // copy line range list FLineCount:=SrcTextStrings.Count; FLineCapacity:=FLineCount; FLineRanges:=AllocMem(FLineCount*SizeOf(TTextLineRange)); SrcItem:=SrcTextStrings.FLineRanges; DstItem:=FLineRanges; for i:=0 to FLineCount-1 do begin DstItem^:=SrcItem^; inc(SrcItem); inc(DstItem); end; FArraysValid:=true; exit; end; end; AddEachLine:=false; if FArraysValid then begin for i:=0 to FLineCount-1 do if FLineRanges[i].TheObject<>nil then begin // old objects have to be kept AddEachLine:=true; break; end; end; if not AddEachLine then begin for i:=0 to TheStrings.Count-1 do begin if TheStrings.Objects[i]<>nil then begin // new objects have to be kept AddEachLine:=true; break; end; end; end; if AddEachLine then begin // append line by line, this can be very slow for i:=0 to TheStrings.Count-1 do AddObject(TheStrings[i],TheStrings.Objects[i]); end else begin // append the whole text at once // Beware: #10,#13 characters in lines are now converted to multiple lines if (FText<>'') and (not (FText[length(FText)] in [#10,#13])) then s:=LineEnding else s:=''; FArraysValid:=false; FText:=FText+s+TheStrings.Text; BuildArrays; end; end; procedure TTextStrings.LoadFromFile(const FileName: string); var TheStream: TFileStreamUTF8; begin TheStream:=TFileStreamUtf8.Create(FileName,fmOpenRead or fmShareDenyWrite); try LoadFromStream(TheStream); finally TheStream.Free; end; end; procedure TTextStrings.SaveToFile(const FileName: string); var TheStream: TFileStreamUTF8; begin TheStream:=TFileStreamUtf8.Create(FileName,fmCreate); try SaveToStream(TheStream); finally TheStream.Free; end; end; end.