1 {
2  /***************************************************************************
3                                 textstrings.pas
4                                 ---------------
5                              Component Library Code
6 
7  ***************************************************************************/
8 
9  *****************************************************************************
10   This file is part of LazUtils.
11 
12   See the file COPYING.modifiedLGPL.txt, included in this distribution,
13   for details about the license.
14  *****************************************************************************
15 
16   TTextStrings is a TStrings descendent that is optimized for handling the
17   complete text as whole (instead of as line by line as in TStringList).
18 
19   TCustomMemoStrings is a TStrings descendent which works around the behavior
20   of TMemo.Lines, which contains the text with wordwrap line endings, in order
21   to store the text in the LFM without those wordwrap line endings. See bug 30659
22 }
23 unit TextStrings;
24 
25 {$mode objfpc}{$H+}
26 
27 interface
28 
29 uses
30   Classes, SysUtils,
31   // LazUtils
32   LazUtf8Classes, LazUtilsStrConsts;
33 
34 type
35   { TTextStrings }
36 
37   TTextLineRange = record
38     Line: string; // cached line as string
39     TheObject: TObject; // user data
40     StartPos: integer; // start of line in Text
41     EndPos: integer; // end of line in Text (= start of newline character(s))
42   end;
43   PTextLineRange = ^TTextLineRange;
44 
45   TCustomMemoStrings = class(TStrings)
46   protected
47     procedure DoReadData(Reader: TReader); virtual;
48     procedure DoWriteData(Writer: TWriter); virtual;
49     procedure DefineProperties(Filer: TFiler); override;
50   end;
51 
52   TTextStrings = class(TCustomMemoStrings)
53   private
54     FOnChange: TNotifyEvent;
55     FOnChanging: TNotifyEvent;
56   protected
57     FArraysValid: boolean;
58     FLineCount: integer;
59     FLineCapacity: integer;
60     FLineRanges: PTextLineRange;// array of TTextLineRange
61     FText: string;
62     FUpdateCount: integer;
63     FChangedWhileUpdate: boolean;
GetTextStrnull64     function GetTextStr: string; override;
65     procedure SetTextStr(const AValue: string); override;
66     procedure BuildArrays; virtual;
GetCountnull67     function GetCount: Integer; override;
68     procedure Changed; virtual;
69     procedure Changing; virtual;
Getnull70     function Get(Index: Integer): string; override;
71     procedure ClearArrays;
GetObjectnull72     function GetObject(Index: Integer): TObject; override;
73     procedure Put(Index: Integer; const S: string); override;
74     procedure PutObject(Index: Integer; AnObject: TObject); override;
GetLineLennull75     function GetLineLen(Index: integer; IncludeNewLineChars: boolean): integer; inline;
GetLineEndnull76     function GetLineEnd(Index: integer; IncludeNewLineChars: boolean): integer;
CountLineEndingsnull77     function CountLineEndings(const s: string): integer;
78   public
79     constructor Create;
80     destructor Destroy; override;
81     procedure Clear; override;
82     procedure SetText(TheText: PChar); override;
83     procedure Insert(Index: Integer; const S: string); override;
84     procedure Delete(Index: Integer); override;
85     procedure Exchange(Index1, Index2: Integer); override;
86     procedure Move(CurIndex, NewIndex: Integer); override;
87     procedure MakeTextBufferUnique;
88     procedure BeginUpdate;
89     procedure EndUpdate;
GetTextnull90     function GetText: PChar; override;
IndexOfnull91     function IndexOf(const S: string): Integer; override;
Addnull92     function Add(const S: string): Integer; override;
AddObjectnull93     function AddObject(const S: string; AObject: TObject): Integer; override;
94     procedure AddStrings(TheStrings: TStrings); override;
95     procedure LoadFromFile(const FileName: string); override;
96     procedure SaveToFile(const FileName: string); override;
97   public
98     property Text: string read FText write SetTextStr;
99     property OnChange: TNotifyEvent read FOnChange write FOnChange;
100     property OnChanging: TNotifyEvent read FOnChanging write FOnChanging;
101   end;
102 
103 implementation
104 
105 { TCustomMemoStrings }
106 
107 procedure TCustomMemoStrings.DoReadData(Reader: TReader);
108 begin
109   Reader.ReadListBegin;
110   BeginUpdate;
111   try
112     Clear;
113     while not Reader.EndOfList do
114       Add(Reader.ReadString);
115   finally
116     EndUpdate;
117   end;
118   Reader.ReadListEnd;
119 end;
120 
121 procedure TCustomMemoStrings.DoWriteData(Writer: TWriter);
122 var
123   i: Integer;
124   lStringsNoWordWrap: TStringList;
125 begin
126   lStringsNoWordWrap := TStringList.Create;
127   try
128     lStringsNoWordWrap.Text := Text;
129 
130     Writer.WriteListBegin;
131     for i := 0 to lStringsNoWordWrap.Count - 1 do
132       Writer.WriteString(lStringsNoWordWrap.Strings[i]);
133     Writer.WriteListEnd;
134   finally
135     lStringsNoWordWrap.Free;
136   end;
137 end;
138 
139 procedure TCustomMemoStrings.DefineProperties(Filer: TFiler);
140 var
141   HasData: Boolean;
142 begin
143   HasData := Count > 0;
144   Filer.DefineProperty('Strings', @DoReadData, @DoWriteData, HasData);
145 end;
146 
147 { TTextStrings }
148 
GetTextStrnull149 function TTextStrings.GetTextStr: string;
150 begin
151   Result:=FText;
152 end;
153 
154 procedure TTextStrings.SetTextStr(const AValue: string);
155 begin
156   if FText=AValue then exit;
157   FText:=AValue;
158   FArraysValid:=false;
159 end;
160 
161 procedure TTextStrings.BuildArrays;
162 var
163   p, line: integer;
164   l: Integer;
165   ArraySize: Integer;
166 begin
167   if FArraysValid then exit;
168   ClearArrays;
169   FArraysValid:=true;
170   // count line ends
171   FLineCount:=CountLineEndings(FText);
172   l:=length(FText);
173   if (FText<>'') and (not (FText[l] in [#10,#13])) then
174     inc(FLineCount);
175   FLineCapacity:=FLineCount;
176   // build line range list
177   if FLineCount>0 then begin
178     ArraySize:=FLineCount*SizeOf(TTextLineRange);
179     GetMem(FLineRanges,ArraySize);
180     FillByte(FLineRanges^,ArraySize,0);
181     p:=1;
182     line:=0;
183     FLineRanges[line].StartPos:=1;
184     FLineRanges[FLineCount-1].EndPos:=l+1;
185     while (p<=l) do begin
186       if (not (FText[p] in [#10,#13])) then begin
187         inc(p);
188       end else begin
189         // new line
190         FLineRanges[line].EndPos:=p;
191         inc(line);
192         inc(p);
193         if (p<=l) and (FText[p] in [#10,#13])
194         and (FText[p]<>FText[p-1]) then
195           inc(p);
196         if line<FLineCount then
197           FLineRanges[line].StartPos:=p;
198       end;
199     end;
200   end;
201 end;
202 
GetCountnull203 function TTextStrings.GetCount: Integer;
204 begin
205   if not FArraysValid then BuildArrays;
206   Result:=FLineCount;
207 end;
208 
209 procedure TTextStrings.Changed;
210 // called after text changed
211 begin
212   if (FUpdateCount>0) then begin
213     FChangedWhileUpdate:=true;
214     exit;
215   end;
216   if Assigned(FOnChange) then
217     FOnChange(Self);
218 end;
219 
220 procedure TTextStrings.Changing;
221 begin
222   if FUpdateCount=0 then
223     if Assigned(FOnChanging) then
224       FOnChanging(Self);
225 end;
226 
Getnull227 function TTextStrings.Get(Index: Integer): string;
228 var
229   Line: PTextLineRange;
230 begin
231   if not FArraysValid then BuildArrays;
232   if (Index<0) or (Index>=FLineCount) then
233     Error(lrsListIndexExceedsBounds, Index);
234   Line:=@FLineRanges[Index];
235   if (Line^.Line='')
236   and (Line^.StartPos<Line^.EndPos) then begin
237     Line^.Line:=copy(FText,Line^.StartPos,Line^.EndPos-Line^.StartPos);
238   end;
239   Result:=Line^.Line;
240 end;
241 
242 procedure TTextStrings.ClearArrays;
243 var
244   i: Integer;
245 begin
246   FArraysValid:=false;
247   if FLineRanges<>nil then begin
248     for i:=0 to FLineCount-1 do
249       FLineRanges[i].Line:='';
250     FreeMem(FLineRanges);
251     FLineRanges:=nil;
252   end;
253   FLineCapacity:=0;
254 end;
255 
TTextStrings.GetObjectnull256 function TTextStrings.GetObject(Index: Integer): TObject;
257 begin
258   if FArraysValid then begin
259     if (Index<0) or (Index>=FLineCount) then
260       Error(lrsListIndexExceedsBounds, Index);
261     Result:=FLineRanges[Index].TheObject;
262   end else
263     Result:=nil;
264 end;
265 
266 procedure TTextStrings.Put(Index: Integer; const S: string);
267 var
268   OldLineLen: Integer;
269   NewLineLen: Integer;
270   Movement: Integer;
271   OldStartPos: LongInt;
272   OldEndPos: LongInt;
273   MoveLen: Integer;
274   i: Integer;
275   NewEndPos: Integer;
276 begin
277   if not FArraysValid then BuildArrays;
278   if (Index<0) or (Index>=FLineCount) then
279     Error(lrsListIndexExceedsBounds, Index);
280   OldStartPos:=FLineRanges[Index].StartPos;
281   OldEndPos:=FLineRanges[Index].EndPos;
282   NewLineLen:=length(s);
283   OldLineLen:=OldEndPos-OldStartPos;
284   Movement:=NewLineLen-OldLineLen;
285   NewEndPos:=OldEndPos+Movement;
286   // move text behind
287   MoveLen := Length(FText) - OldEndPos + 1;
288   if (Movement<>0) and (MoveLen>0) then
289   begin
290     if Movement > 0 then
291       SetLength(FText, Length(FText) + Movement);
292     System.Move(FText[OldEndPos], FText[NewEndPos], MoveLen);
293     if Movement < 0 then
294       SetLength(FText, Length(FText) + Movement);
295 
296     for i := Index + 1 to FLineCount - 1 do
297     begin
298       inc(FLineRanges[i].StartPos, Movement);
299       inc(FLineRanges[i].EndPos, Movement);
300     end;
301   end;
302   FLineRanges[Index].EndPos:=NewEndPos;
303   // copy text
304   if NewLineLen>0 then
305     System.Move(S[1],FText[OldStartPos],NewLineLen);
306   FLineRanges[Index].Line:=S;
307   // check if arrays need rebuild
308   i:=NewLineLen;
309   while (i>0) and (not (S[i] in [#10,#13])) do dec(i);
310   if i>0 then begin
311     // S contains new line chars => rebuild needed
312     FArraysValid:=false;
313   end;
314 end;
315 
316 procedure TTextStrings.PutObject(Index: Integer; AnObject: TObject);
317 begin
318   if not FArraysValid then BuildArrays;
319   if (Index<0) or (Index>=FLineCount) then
320     Error(lrsListIndexExceedsBounds, Index);
321   FLineRanges[Index].TheObject:=AnObject;
322 end;
323 
GetLineLennull324 function TTextStrings.GetLineLen(Index: integer; IncludeNewLineChars: boolean
325   ): integer;
326 begin
327   Result:=GetLineEnd(Index,IncludeNewLineChars)-FLineRanges[Index].StartPos;
328 end;
329 
TTextStrings.GetLineEndnull330 function TTextStrings.GetLineEnd(Index: integer; IncludeNewLineChars: boolean
331   ): integer;
332 begin
333   if not FArraysValid then BuildArrays;
334   if not IncludeNewLineChars then
335     Result:=FLineRanges[Index].EndPos
336   else if Index=FLineCount-1 then
337     Result:=length(FText)+1
338   else
339     Result:=FLineRanges[Index+1].StartPos;
340 end;
341 
TTextStrings.CountLineEndingsnull342 function TTextStrings.CountLineEndings(const s: string): integer;
343 var
344   p: Integer;
345   l: Integer;
346 begin
347   Result:=0;
348   l:=length(s);
349   p:=1;
350   while p<=l do begin
351     if s[p] in [#10,#13] then
352     begin
353       inc(Result);
354       inc(p);
355       if (p<=l) and (s[p] in [#10,#13]) and (s[p-1]<>s[p]) then
356         inc(p);
357     end else begin
358       inc(p);
359     end;
360   end;
361 end;
362 
363 constructor TTextStrings.Create;
364 begin
365   inherited Create;
366   CheckSpecialChars;
367 end;
368 
369 destructor TTextStrings.Destroy;
370 begin
371   Clear;
372   inherited Destroy;
373 end;
374 
375 procedure TTextStrings.Clear;
376 begin
377   ClearArrays;
378   FLineCount:=0;
379   FText:='';
380 end;
381 
382 procedure TTextStrings.SetText(TheText: PChar);
383 begin
384   if FText=TheText then exit;
385   FText:=TheText;
386   FArraysValid:=false;
387 end;
388 
389 procedure TTextStrings.Insert(Index: Integer; const S: string);
390 
391   procedure RaiseOutOfBounds;
392   begin
393     raise EListError.Create('insert index '+IntToStr(Index)+' out of bounds '+IntToStr(FLineCount));
394   end;
395 
396 var
397   NewStartPos: Integer;
398   NewLineCharCount: Integer;
399   NewLineLen: Integer;
400   i: Integer;
401   SEndsInNewLine: boolean;
402   Range: PTextLineRange;
403   NewCapacity: Integer;
404 begin
405   if not FArraysValid then BuildArrays;
406   NewLineLen:=length(S);
407   SEndsInNewLine:=(S<>'') and (S[NewLineLen] in [#10,#13]);
408   if Index<FLineCount then
409   begin
410     if Index<0 then
411       RaiseOutOfBounds;
412     NewStartPos:=FLineRanges[Index].StartPos;
413   end else begin
414     if Index>FLineCount then
415       RaiseOutOfBounds;
416     NewStartPos:=length(FText)+1;
417   end;
418   NewLineCharCount:=0;
419   if SEndsInNewLine then begin
420     inc(NewLineCharCount);
421     if (NewLineLen>1)
422     and (S[NewLineLen-1] in [#10,#13])
423     and (S[NewLineLen-1]<>S[NewLineLen]) then
424       inc(NewLineCharCount);
425     System.Insert(S,FText,NewStartPos);
426   end else begin
427     // append missing newline char
428     System.Insert(S+LineEnding,FText,NewStartPos);
429     NewLineCharCount:=length(LineEnding);
430     inc(NewLineLen,NewLineCharCount);
431   end;
432   // adjust arrays
433   if FLineCount=FLineCapacity then begin
434     if FLineCapacity<8 then
435       NewCapacity:=8
436     else
437       NewCapacity:=FLineCapacity shl 1;
438     ReAllocMem(FLineRanges,SizeOf(TTextLineRange)*NewCapacity);
439     FillByte(FLineRanges[FLineCapacity],SizeOf(TTextLineRange)*(NewCapacity-FLineCapacity),0);
440     FLineCapacity:=NewCapacity;
441   end;
442   if Index<FLineCount then begin
443     System.Move(FLineRanges[Index],FLineRanges[Index+1],
444                 (FLineCount-Index)*SizeOf(TTextLineRange));
445     FillByte(FLineRanges[Index],SizeOf(TTextLineRange),0);
446     for i:=Index+1 to FLineCount do begin
447       inc(FLineRanges[i].StartPos,NewLineLen);
448       inc(FLineRanges[i].EndPos,NewLineLen);
449     end;
450   end;
451   inc(FLineCount);
452   Range:=@FLineRanges[Index];
453   Range^.Line:=S;
454   Range^.StartPos:=NewStartPos;
455   Range^.EndPos:=NewStartPos+NewLineLen-NewLineCharCount;
456 end;
457 
458 procedure TTextStrings.Delete(Index: Integer);
459 var
460   OldLineLen: Integer;
461   OldStartPos: Integer;
462   i: Integer;
463 begin
464   if not FArraysValid then BuildArrays;
465   if (Index<0) or (Index>=FLineCount) then
466     Error(lrsListIndexExceedsBounds, Index);
467   // adjust text
468   OldLineLen:=GetLineLen(Index,true);
469   if OldLineLen>0 then begin
470     OldStartPos:=FLineRanges[Index].StartPos;
471     System.Delete(FText,OldStartPos,OldLineLen);
472   end;
473   // adjust arrays
474   dec(FLineCount);
475   FLineRanges[Index].Line:='';
476   if Index<FLineCount then begin
477     System.Move(FLineRanges[Index+1],FLineRanges[Index],
478          (FLineCount-Index)*SizeOf(TTextLineRange));
479     for i:=Index to FLineCount-1 do begin
480       dec(FLineRanges[i].StartPos,OldLineLen);
481       dec(FLineRanges[i].EndPos,OldLineLen);
482     end;
483   end;
484   // clear last element
485   FillByte(FLineRanges[FLineCount],SizeOf(TTextLineRange),0);
486 end;
487 
488 procedure TTextStrings.Exchange(Index1, Index2: Integer);
489 var
490   LineLen1: Integer;
491   LineLen2: Integer;
492   buf: Pointer;
493   Dummy: Integer;
494   OldBetweenStart: Integer;
495   NewBetweenStart: Integer;
496   BetweenLength: Integer;
497   StartPos1: LongInt;
498   StartPos2: LongInt;
499   i: Integer;
500   Movement: Integer;
501   Obj: TObject;
502   LineShortLen1: LongInt;
503   LineShortLen2: LongInt;
504   Line1: PTextLineRange;
505   Line2: PTextLineRange;
506 begin
507   // check values
508   if Index1=Index2 then exit;
509   if Index1<0 then
510     Error(lrsListIndexExceedsBounds, Index1);
511   if Index2<0 then
512     Error(lrsListIndexExceedsBounds, Index2);
513   if not FArraysValid then BuildArrays;
514   if Index1>=FLineCount then
515     Error(lrsListIndexExceedsBounds, Index1);
516   if Index2>=FLineCount then
517     Error(lrsListIndexExceedsBounds, Index2);
518 
519   // make sure Index1<Index2
520   if Index1>Index2 then begin
521     Dummy:=Index1;
522     Index1:=Index2;
523     Index2:=Dummy;
524   end;
525 
526   Line1:=@FLineRanges[Index1];
527   Line2:=@FLineRanges[Index2];
528 
529   // adjust text
530   MakeTextBufferUnique;
531 
532   if (Index2=FLineCount-1) and (Line2^.EndPos>length(FText))
533   then begin
534     // The last line should be exchanged,
535     // but Text has no new line character(s) at the end
536     // => add LineEnding
537     FText:=FText+LineEnding;
538   end;
539 
540   // get line lengths including new line chars
541   LineLen1:=GetLineLen(Index1,true);
542   LineLen2:=GetLineLen(Index2,true);
543   if (LineLen1<1) and (LineLen2<1) then exit;
544   LineShortLen1:=GetLineLen(Index1,false);
545   LineShortLen2:=GetLineLen(Index2,false);
546 
547   // save the bigger line
548   StartPos1:=Line1^.StartPos;
549   StartPos2:=Line2^.StartPos;
550   if LineLen1>=LineLen2 then begin
551     GetMem(buf,LineLen1);
552     System.Move(FText[StartPos1],buf^,LineLen1);
553   end else begin
554     GetMem(buf,LineLen2);
555     System.Move(FText[StartPos2],buf^,LineLen2);
556   end;
557 
558   // move text in between
559   OldBetweenStart:=StartPos1+LineLen1;
560   BetweenLength:=StartPos2-OldBetweenStart;
561   NewBetweenStart:=StartPos1+LineLen2;
562   Movement:=NewBetweenStart-OldBetweenStart;
563   if (BetweenLength>0) and (Movement<>0) then
564     System.Move(FText[OldBetweenStart],FText[NewBetweenStart],BetweenLength);
565 
566   // move both lines
567   Line1^.Line:='';
568   Line2^.Line:='';
569   if LineLen1>=LineLen2 then begin
570     System.Move(FText[StartPos2],FText[StartPos1],LineLen2);
571     System.Move(buf^,FText[StartPos2+Movement],LineLen1);
572   end else begin
573     System.Move(FText[StartPos1],FText[StartPos2+Movement],LineLen1);
574     System.Move(buf^,FText[StartPos1],LineLen2);
575   end;
576 
577   // adjust line ranges
578   if Movement<>0 then
579   begin
580     Line1^.EndPos:=Line1^.StartPos+LineShortLen2;
581     inc(Line2^.StartPos,Movement);
582     Line2^.EndPos:=Line2^.StartPos+LineShortLen1;
583     for i:=Index1+1 to Index2-1 do begin
584       inc(FLineRanges[i].StartPos,Movement);
585       inc(FLineRanges[i].EndPos,Movement);
586     end;
587   end;
588 
589   // exchange TheObject
590   Obj:=Line1^.TheObject;
591   Line1^.TheObject:=Line2^.TheObject;
592   Line2^.TheObject:=Obj;
593 
594   // clean up
595   FreeMem(buf);
596 end;
597 
598 procedure TTextStrings.Move(CurIndex, NewIndex: Integer);
599 var
600   SrcPos1: LongInt;
601   SrcPos2: LongInt;
602   SrcPos3: LongInt;
603   LineStr: String;
604   LineLen: Integer;
605   i: LongInt;
606   Obj: TObject;
607   LineShortLen: LongInt;
608   Line: PTextLineRange;
609 begin
610   // check values
611   if CurIndex=NewIndex then exit;
612   if CurIndex<0 then
613     Error(lrsListIndexExceedsBounds, CurIndex);
614   if NewIndex<0 then
615     Error(lrsListIndexExceedsBounds, NewIndex);
616   if not FArraysValid then BuildArrays;
617   if CurIndex>=FLineCount then
618     Error(lrsListIndexExceedsBounds, CurIndex);
619   if NewIndex>=FLineCount then
620     Error(lrsListIndexExceedsBounds, NewIndex);
621 
622   // adjust text
623   MakeTextBufferUnique;
624 
625   if CurIndex<NewIndex then
626   begin
627     // move to higher index
628     if (NewIndex=FLineCount-1) and (FLineRanges[NewIndex].EndPos>length(FText))
629     then begin
630       // CurIndex should be moved to the end,
631       // but Text has no new line character(s) at the end
632       // => add LineEnding
633       FText:=FText+LineEnding;
634     end;
635     SrcPos1:=FLineRanges[CurIndex].StartPos;
636     SrcPos2:=FLineRanges[CurIndex+1].StartPos;
637     SrcPos3:=GetLineEnd(NewIndex,true);
638     // store current line with line end
639     LineLen:=SrcPos2-SrcPos1;
640     LineShortLen:=GetLineLen(CurIndex,false);
641     LineStr:=copy(FText,SrcPos1,LineLen);
642     Obj:=FLineRanges[CurIndex].TheObject;
643     // move lines -1
644     System.Move(FText[SrcPos2],FText[SrcPos1],SrcPos3-SrcPos2);
645     for i:=CurIndex+1 to NewIndex do begin
646       dec(FLineRanges[i].StartPos,LineLen);
647       dec(FLineRanges[i].EndPos,LineLen);
648     end;
649     System.Move(FLineRanges[CurIndex+1],FLineRanges[CurIndex],
650                 SizeOf(TTextLineRange)*(NewIndex-CurIndex));
651     // put current line at new position
652     i:=SrcPos3-LineLen;
653     System.Move(LineStr[1],FText[i],LineLen);
654     Line:=@FLineRanges[NewIndex];
655     Line^.StartPos:=i;
656     Line^.EndPos:=i+LineShortLen;
657     Pointer(Line^.Line):=nil; // this will be updated on demand, see Get
658     Line^.TheObject:=Obj;
659   end else begin
660     // move to lower index
661     if (CurIndex=FLineCount-1) and (FLineRanges[CurIndex].EndPos>length(FText))
662     then begin
663       // CurIndex should be moved from the end,
664       // but Text has no new line character(s) at the end
665       // => add LineEnding
666       FText:=FText+LineEnding;
667     end;
668     SrcPos1:=FLineRanges[NewIndex].StartPos;
669     SrcPos2:=FLineRanges[CurIndex].StartPos;
670     SrcPos3:=GetLineEnd(CurIndex,true);
671     // store current line with line end
672     LineLen:=SrcPos3-SrcPos2;
673     LineShortLen:=GetLineLen(CurIndex,false);
674     LineStr:=copy(FText,SrcPos2,LineLen);
675     Obj:=FLineRanges[CurIndex].TheObject;
676     // move lines +1
677     System.Move(FText[SrcPos1],FText[SrcPos1+LineLen],SrcPos2-SrcPos1);
678     for i:=CurIndex-1 downto NewIndex do begin
679       inc(FLineRanges[i].StartPos,LineLen);
680       inc(FLineRanges[i].EndPos,LineLen);
681     end;
682     System.Move(FLineRanges[NewIndex],FLineRanges[NewIndex+1],
683                 SizeOf(TTextLineRange)*(CurIndex-NewIndex));
684     // put current line at new position
685     System.Move(LineStr[1],FText[SrcPos1],LineLen);
686     Line:=@FLineRanges[NewIndex];
687     Line^.StartPos:=SrcPos1;
688     Line^.EndPos:=SrcPos1+LineShortLen;
689     Pointer(Line^.Line):=nil; // this will be updated on demand, see Get
690     Line^.TheObject:=Obj;
691   end;
692 end;
693 
694 procedure TTextStrings.MakeTextBufferUnique;
695 begin
696   // make string unique (refcount=1) to be able to edit it directly
697   UniqueString(FText);
698 end;
699 
700 procedure TTextStrings.BeginUpdate;
701 begin
702   inc(FUpdateCount);
703 end;
704 
705 procedure TTextStrings.EndUpdate;
706 
707   procedure RaiseUpdateCount;
708   begin
709     raise Exception.Create('TTextStrings.EndUpdate');
710   end;
711 
712 begin
713   if FUpdateCount<=0 then RaiseUpdateCount;
714   dec(FUpdateCount);
715   if FUpdateCount=0 then begin
716     if FChangedWhileUpdate then
717       Changed;
718   end;
719 end;
720 
TTextStrings.GetTextnull721 function TTextStrings.GetText: PChar;
722 begin
723   Result:=PChar(FText);
724 end;
725 
IndexOfnull726 function TTextStrings.IndexOf(const S: string): Integer;
727 begin
728   Result:=inherited IndexOf(S);
729 end;
730 
Addnull731 function TTextStrings.Add(const S: string): Integer;
732 begin
733   Result:=AddObject(S,nil);
734 end;
735 
TTextStrings.AddObjectnull736 function TTextStrings.AddObject(const S: string; AObject: TObject): Integer;
737 var
738   e: String;
739   NewLineCount: Integer;
740   OldTxtLen: Integer;
741   p: Integer;
742   l: Integer;
743 begin
744   Result:=Count;
745   if (FText<>'') and (not (FText[length(FText)] in [#10,#13])) then
746     e:=LineEnding
747   else
748     e:='';
749   OldTxtLen:=length(FText);
750   FText:=Text+e+S+LineEnding;
751   if AObject<>nil then
752     BuildArrays;
753   if FArraysValid then
754   begin
755     // update FLineRanges
756     NewLineCount:=FLineCount+CountLineEndings(S)+1;
757     if NewLineCount>FLineCapacity then begin
758       FLineCapacity:=FLineCapacity*2+10;
759       if FLineCapacity<NewLineCount then
760         FLineCapacity:=NewLineCount;
761       ReAllocMem(FLineRanges,SizeOf(TTextLineRange)*FLineCapacity);
762       FillByte(FLineRanges[FLineCount],SizeOf(TTextLineRange)*(FLineCapacity-FLineCount),0);
763     end;
764     FLineRanges[FLineCount].TheObject:=AObject;
765     p:=OldTxtLen+length(e)+1;
766     l:=length(FText);
767     while FLineCount<NewLineCount do begin
768       FLineRanges[FLineCount].StartPos:=p;
769       while (p<=l) and (not (FText[p] in [#10,#13])) do
770         inc(p);
771       FLineRanges[FLineCount].EndPos:=p;
772       inc(p);
773       if (p<=l) and (FText[p] in [#10,#13]) and (FText[p]<>FText[p-1]) then
774         inc(p);
775       inc(FLineCount);
776     end;
777   end;
778 end;
779 
780 procedure TTextStrings.AddStrings(TheStrings: TStrings);
781 var
782   s: String;
783   i: Integer;
784   AddEachLine: Boolean;
785   SrcTextStrings: TTextStrings;
786   SrcItem: PTextLineRange;
787   DstItem: PTextLineRange;
788 begin
789   if TheStrings.Count=0 then exit;
790   if FLineCount=0 then begin
791     if TheStrings is TTextStrings then begin
792       // copy Text, lineranges
793       SrcTextStrings:=TTextStrings(TheStrings);
794       FText:=SrcTextStrings.Text;
795       ClearArrays;
796       if not SrcTextStrings.FArraysValid then exit;
797       // copy line range list
798       FLineCount:=SrcTextStrings.Count;
799       FLineCapacity:=FLineCount;
800       FLineRanges:=AllocMem(FLineCount*SizeOf(TTextLineRange));
801       SrcItem:=SrcTextStrings.FLineRanges;
802       DstItem:=FLineRanges;
803       for i:=0 to FLineCount-1 do begin
804         DstItem^:=SrcItem^;
805         inc(SrcItem);
806         inc(DstItem);
807       end;
808       FArraysValid:=true;
809       exit;
810     end;
811   end;
812   AddEachLine:=false;
813   if FArraysValid then begin
814     for i:=0 to FLineCount-1 do
815       if FLineRanges[i].TheObject<>nil then begin
816         // old objects have to be kept
817         AddEachLine:=true;
818         break;
819       end;
820   end;
821   if not AddEachLine then begin
822     for i:=0 to TheStrings.Count-1 do begin
823       if TheStrings.Objects[i]<>nil then begin
824         // new objects have to be kept
825         AddEachLine:=true;
826         break;
827       end;
828     end;
829   end;
830   if AddEachLine then begin
831     // append line by line, this can be very slow
832     for i:=0 to TheStrings.Count-1 do
833       AddObject(TheStrings[i],TheStrings.Objects[i]);
834   end else begin
835     // append the whole text at once
836     // Beware: #10,#13 characters in lines are now converted to multiple lines
837     if (FText<>'') and (not (FText[length(FText)] in [#10,#13])) then
838       s:=LineEnding
839     else
840       s:='';
841     FArraysValid:=false;
842     FText:=FText+s+TheStrings.Text;
843     BuildArrays;
844   end;
845 end;
846 
847 procedure TTextStrings.LoadFromFile(const FileName: string);
848 var
849   TheStream: TFileStreamUTF8;
850 begin
851   TheStream:=TFileStreamUtf8.Create(FileName,fmOpenRead or fmShareDenyWrite);
852   try
853     LoadFromStream(TheStream);
854   finally
855     TheStream.Free;
856   end;
857 end;
858 
859 procedure TTextStrings.SaveToFile(const FileName: string);
860 var
861   TheStream: TFileStreamUTF8;
862 begin
863   TheStream:=TFileStreamUtf8.Create(FileName,fmCreate);
864   try
865     SaveToStream(TheStream);
866   finally
867     TheStream.Free;
868   end;
869 end;
870 
871 end.
872