1 {-------------------------------------------------------------------------------
2 The contents of this file are subject to the Mozilla Public License
3 Version 1.1 (the "License"); you may not use this file except in compliance
4 with the License. You may obtain a copy of the License at
5 http://www.mozilla.org/MPL/
6 
7 Software distributed under the License is distributed on an "AS IS" basis,
8 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
9 the specific language governing rights and limitations under the License.
10 
11 Alternatively, the contents of this file may be used under the terms of the
12 GNU General Public License Version 2 or later (the "GPL"), in which case
13 the provisions of the GPL are applicable instead of those above.
14 If you wish to allow use of your version of this file only under the terms
15 of the GPL and not to allow others to use your version of this file
16 under the MPL, indicate your decision by deleting the provisions above and
17 replace them with the notice and other provisions required by the GPL.
18 If you do not delete the provisions above, a recipient may use your version
19 of this file under either the MPL or the GPL.
20 
21 -------------------------------------------------------------------------------}
22 unit SynEditTextTabExpander;
23 
24 {$I synedit.inc}
25 
26 interface
27 
28 uses
29   LCLProc, Classes, SysUtils, math, LazSynEditText, SynEditTextBase;
30 
31 type
32 
33   // lines longer than 16383 chars, will be stored as unknown
34   TLineLen = Word;
35   PLineLen = ^TLineLen;
36 
37   { TSynEditStringTabData }
38 
39   TSynEditStringTabData = class(TSynManagedStorageMem)
40   private
41     FRefCount: Integer;
GetLineLennull42     function GetLineLen(Index: Integer): TLineLen;
43     procedure SetLineLen(Index: Integer; const AValue: TLineLen);
44   public
45     constructor Create;
46     procedure IncRefCount;
47     procedure DecRefCount;
48     property RefCount: Integer read FRefCount;
49     property LineLen[Index: Integer]: TLineLen read GetLineLen write SetLineLen; default;
50   end;
51 
52 { TSynEditStringTabExpander }
53 
54   TSynEditStringTabExpander = class(TSynEditStringsLinked)
55   private
56     FTabWidth: integer;
57     FIndexOfLongestLine: Integer;
58     FFirstUnknownLongestLine, FLastUnknownLongestLine: Integer;
59     FTabData: TSynEditStringTabData;
60     FLastLineHasTab: Boolean; // Last line, parsed by GetPhysicalCharWidths
61     FLastLinePhysLen: Integer;
62     FViewChangeStamp: int64;
63     procedure TextBufferChanged(Sender: TObject);
64     procedure LineTextChanged(Sender: TSynEditStrings; aIndex, aCount: Integer);
65     procedure LineCountChanged(Sender: TSynEditStrings; AIndex, ACount : Integer);
ExpandedStringnull66     function ExpandedString(Index: integer): string;
ExpandedStringLengthnull67     function ExpandedStringLength(Index: integer): Integer;
68   protected
GetViewChangeStampnull69     function GetViewChangeStamp: int64; override;
GetTabWidthnull70     function  GetTabWidth : integer;
71     procedure SetTabWidth(const AValue : integer);
GetExpandedStringnull72     function  GetExpandedString(Index: integer): string; override;
GetLengthOfLongestLinenull73     function  GetLengthOfLongestLine: integer; override;
74     procedure DoGetPhysicalCharWidths(Line: PChar; LineLen, Index: Integer; PWidths: PPhysicalCharWidth); override;
75   public
76     constructor Create(ASynStringSource: TSynEditStrings);
77     destructor Destroy; override;
78 
79     property LengthOfLongestLine: integer read GetLengthOfLongestLine;
80   public
81     property TabWidth: integer read GetTabWidth write SetTabWidth;
82   end;
83 
84 
85 implementation
86 
87 const
88   // Offset to add to LengthOfLine, if Line has no tabs.
89   // (Length will still be valid if tab-width changes)
90   NO_TAB_IN_LINE_OFFSET = high(TLineLen) div 2;
91   LINE_LEN_UNKNOWN = high(TLineLen);
92   MAX_LINE_LEN_STORED = NO_TAB_IN_LINE_OFFSET - 1;
93 
GetHasTabsnull94 function GetHasTabs(pLine: PChar): boolean;
95 begin
96   if Assigned(pLine) then begin
97     while (pLine^ <> #0) do begin
98       if (pLine^ = #9) then break;
99       Inc(pLine);
100     end;
101     Result := (pLine^ = #9);
102   end else
103     Result := FALSE;
104 end;
105 
106 { TSynEditStringTabData }
107 
GetLineLennull108 function TSynEditStringTabData.GetLineLen(Index: Integer): TLineLen;
109 begin
110   Result := PLineLen(ItemPointer[Index])^;
111 end;
112 
113 procedure TSynEditStringTabData.SetLineLen(Index: Integer; const AValue: TLineLen);
114 begin
115   PLineLen(ItemPointer[Index])^ := AValue;
116 end;
117 
118 constructor TSynEditStringTabData.Create;
119 begin
120   inherited;
121   ItemSize := SizeOf(TLineLen);
122   FRefCount := 1;
123 end;
124 
125 procedure TSynEditStringTabData.IncRefCount;
126 begin
127   inc(FRefCount);
128 end;
129 
130 procedure TSynEditStringTabData.DecRefCount;
131 begin
132   dec(FRefCount);
133 end;
134 
135 { TSynEditStringTabExpander }
136 
137 constructor TSynEditStringTabExpander.Create(ASynStringSource: TSynEditStrings);
138 begin
139   FIndexOfLongestLine := -1;
140   FFirstUnknownLongestLine := -1;
141   FLastUnknownLongestLine := -1;
142   inherited Create(ASynStringSource);
143   TextBufferChanged(nil);
144   TabWidth := 8;
145   fSynStrings.AddChangeHandler(senrLineCount, @LineCountChanged);
146   fSynStrings.AddChangeHandler(senrLineChange, @LineTextChanged);
147   fSynStrings.AddNotifyHandler(senrTextBufferChanged, @TextBufferChanged);
148 end;
149 
150 destructor TSynEditStringTabExpander.Destroy;
151 var
152   Data: TSynEditStringTabData;
153 begin
154   Data := TSynEditStringTabData(fSynStrings.Ranges[Self]);
155   if Assigned(Data) then begin
156     Data.DecRefCount;
157     if Data.RefCount = 0 then begin
158       fSynStrings.Ranges[Self] := nil;
159       Data.Free;
160     end;
161   end;
162   fSynStrings.RemoveChangeHandler(senrLineChange, @LineTextChanged);
163   fSynStrings.RemoveChangeHandler(senrLineCount, @LineCountChanged);
164   fSynStrings.RemoveNotifyHandler(senrTextBufferChanged, @TextBufferChanged);
165   inherited Destroy;
166 end;
167 
TSynEditStringTabExpander.GetTabWidthnull168 function TSynEditStringTabExpander.GetTabWidth: integer;
169 begin
170   Result := FTabWidth;
171 end;
172 
173 procedure TSynEditStringTabExpander.SetTabWidth(const AValue: integer);
174 var
175   i: integer;
176 begin
177   if FTabWidth = AValue then exit;
178 
179   {$PUSH}{$Q-}{$R-}
180   FViewChangeStamp := FViewChangeStamp + 1;
181   {$POP}
182 
183   FTabWidth := AValue;
184   FIndexOfLongestLine := -1;
185   FFirstUnknownLongestLine := -1;
186   FLastUnknownLongestLine := -1;
187   for i := 0 to Count - 1 do
188     if not(FTabData[i] >= NO_TAB_IN_LINE_OFFSET) then
189       FTabData[i] := LINE_LEN_UNKNOWN;
190 end;
191 
GetViewChangeStampnull192 function TSynEditStringTabExpander.GetViewChangeStamp: int64;
193 begin
194   Result := inherited GetViewChangeStamp;
195   {$PUSH}{$Q-}{$R-}
196   Result := Result + FViewChangeStamp;
197   {$POP}
198 end;
199 
200 procedure TSynEditStringTabExpander.TextBufferChanged(Sender: TObject);
201 var
202   Data: TSynEditStringTabData;
203 begin
204   // Using self, instead as class, to register tab-width-data
205   // other shared edits can have different tab-width
206   if (Sender <> nil) and
207      (FTabData = TSynEditStringTabData(NextLines.Ranges[Self]))
208   then
209     exit;
210 
211   if Sender <> nil then begin
212     Data := TSynEditStringTabData(TSynEditStrings(Sender).Ranges[Self]);
213     if Assigned(Data) then begin
214       Data.DecRefCount;
215       if Data.RefCount = 0 then begin
216         TSynEditStrings(Sender).Ranges[Self] := nil;
217         Data.Free;
218       end;
219     end;
220   end;
221   FTabData := TSynEditStringTabData(NextLines.Ranges[Self]);
222   if FTabData = nil then begin
223     FTabData := TSynEditStringTabData.Create;
224     NextLines.Ranges[Self] := FTabData;
225   end
226   else
227     FTabData.IncRefCount;
228   LineTextChanged(TSynEditStrings(Sender), 0, Count);
229 end;
230 
231 procedure TSynEditStringTabExpander.LineTextChanged(Sender: TSynEditStrings; aIndex,
232   aCount: Integer);
233 var
234   i: integer;
235 begin
236   if (FIndexOfLongestLine >= AIndex) and (FIndexOfLongestLine < AIndex+ACount) then
237     FIndexOfLongestLine := -1;
238   if (FFirstUnknownLongestLine < 0) or (AIndex < FFirstUnknownLongestLine) then
239     FFirstUnknownLongestLine := AIndex;
240   if AIndex+ACount-1 > FLastUnknownLongestLine then
241     FLastUnknownLongestLine := AIndex+ACount-1;
242   for i := AIndex to AIndex + ACount - 1 do
243     FTabData[i] := LINE_LEN_UNKNOWN;
244 end;
245 
246 procedure TSynEditStringTabExpander.LineCountChanged(Sender: TSynEditStrings; AIndex, ACount: Integer);
247 var
248   i: integer;
249 begin
250   if (FIndexOfLongestLine >= AIndex) then
251     FIndexOfLongestLine := FIndexOfLongestLine + ACount;
252 
253   if ACount < 0 then begin
254     if (FIndexOfLongestLine >= AIndex+ACount) and (FIndexOfLongestLine < AIndex) then
255       FIndexOfLongestLine := -1;
256     if (FFirstUnknownLongestLine >= 0) then begin
257       if (AIndex < FFirstUnknownLongestLine) then
258         FFirstUnknownLongestLine := Max(AIndex, FFirstUnknownLongestLine + ACount);
259       if (AIndex < FLastUnknownLongestLine) then
260         FLastUnknownLongestLine := Max(AIndex, FLastUnknownLongestLine + ACount);
261     end;
262 
263     exit;
264   end;
265 
266   if (FFirstUnknownLongestLine < 0) or (AIndex < FFirstUnknownLongestLine) then
267     FFirstUnknownLongestLine := AIndex;
268   if (AIndex < FLastUnknownLongestLine) or (FLastUnknownLongestLine < 0) then
269     FLastUnknownLongestLine := Max(AIndex, FLastUnknownLongestLine) +ACount;
270   for i := AIndex to AIndex + ACount - 1 do
271     FTabData[i] := LINE_LEN_UNKNOWN;
272 end;
273 
ExpandedStringnull274 function TSynEditStringTabExpander.ExpandedString(Index: integer): string;
275 var
276   Line: String;
277   CharWidths: TPhysicalCharWidths;
278   i, j, l: Integer;
279 begin
280 // this is only used by trimmer.lengthOfLongestLine / which is not called, if a tab module is present
281   Line := fSynStrings[Index];
282   if (Line = '') or (not GetHasTabs(PChar(Line))) then begin
283     Result := Line;
284     // xxx wrong double width // none latin ...
285     //FTabData[Index] := length(Result) + NO_TAB_IN_LINE_OFFSET;
286   end else begin
287     CharWidths := GetPhysicalCharWidths(Pchar(Line), length(Line), Index);
288     l := 0;
289     for i := 0 to length(CharWidths)-1 do
290       l := l + (CharWidths[i] and PCWMask);
291     SetLength(Result, l);
292 
293     l := 1;
294     for i := 1 to length(CharWidths) do begin
295       if Line[i] <> #9 then begin
296         Result[l] := Line[i];
297         inc(l);
298       end else begin
299         for j := 1 to (CharWidths[i-1] and PCWMask) do begin
300           Result[l] := ' ';
301           inc(l);
302         end;
303       end;
304     end;
305     FTabData[Index] := length(Result);
306   end;
307 end;
308 
ExpandedStringLengthnull309 function TSynEditStringTabExpander.ExpandedStringLength(Index: integer): Integer;
310 var
311   Line: String;
312   CharWidths: TPhysicalCharWidths;
313   i: Integer;
314 begin
315   Line := fSynStrings[Index];
316   if (Line = '') then begin
317     Result := 0;
318     FTabData[Index] := Result + NO_TAB_IN_LINE_OFFSET;
319   end else begin
320     i := length(Line);
321     SetLength(CharWidths, i);
322     DoGetPhysicalCharWidths(Pchar(Line), i, Index, @CharWidths[0]);
323     Result := 0;
324     for i := 0 to length(CharWidths)-1 do
325       Result := Result + (CharWidths[i] and PCWMask);
326 
327     if FLastLineHasTab then // FLastLineHasTab is set by GetPhysicalCharWidths
328       FTabData[Index] := Result
329     else
330       FTabData[Index] := Result + NO_TAB_IN_LINE_OFFSET;
331   end;
332 end;
333 
TSynEditStringTabExpander.GetExpandedStringnull334 function TSynEditStringTabExpander.GetExpandedString(Index: integer): string;
335 begin
336   if (Index >= 0) and (Index < Count) then begin
337     if FTabData[Index] >= NO_TAB_IN_LINE_OFFSET then
338       Result := fSynStrings[Index]
339     else
340       Result := ExpandedString(Index);
341   end else
342     Result := '';
343 end;
344 
345 procedure TSynEditStringTabExpander.DoGetPhysicalCharWidths(Line: PChar;
346   LineLen, Index: Integer; PWidths: PPhysicalCharWidth);
347 var
348   HasTab: Boolean;
349   i, j: Integer;
350 begin
351   inherited DoGetPhysicalCharWidths(Line, LineLen, Index, PWidths);
352   HasTab := False;
353   j := 0;
354   for i := 0 to LineLen - 1 do begin
355     if (PWidths^ and PCWMask) <> 0 then begin
356       if Line^ = #9 then begin
357         PWidths^ := (FTabWidth - (j mod FTabWidth) and PCWMask) or (PWidths^  and (not PCWMask));
358         HasTab := True;
359       end;
360       j := j + (PWidths^ and PCWMask);
361     end;
362     inc(Line);
363     inc(PWidths);
364   end;
365   FLastLineHasTab := HasTab;
366   FLastLinePhysLen := j;
367 end;
368 
GetLengthOfLongestLinenull369 function TSynEditStringTabExpander.GetLengthOfLongestLine: integer;
370 var
371   Line: PChar;
372   LineLen: Integer;
373   CharWidths: PPhysicalCharWidth;
374   i, j, m: Integer;
375   Line1, Line2: Integer;
376 begin
377   Result := 0;
378   Line1 := 0;
379   Line2 := Count - 1;
380 
381   if (fIndexOfLongestLine >= 0) and (fIndexOfLongestLine < Count) then begin
382     Result := FTabData[fIndexOfLongestLine];
383     if Result <> LINE_LEN_UNKNOWN then begin
384       if Result >= NO_TAB_IN_LINE_OFFSET then Result := Result -  NO_TAB_IN_LINE_OFFSET;
385       if (FFirstUnknownLongestLine < 0) then
386         exit;
387       // Result has the value from index
388       Line1 := FFirstUnknownLongestLine;
389       if (FLastUnknownLongestLine < Line2) then
390         Line2 := FLastUnknownLongestLine;
391     end
392     else begin
393       Result := 0;
394       if (FFirstUnknownLongestLine < 0) then begin
395         Line1 := fIndexOfLongestLine;
396         Line2 := fIndexOfLongestLine;
397       end
398       else begin // TODO: Calculate for fIndexOfLongestLine, instead of extending the range
399         Line1 := Min(fIndexOfLongestLine, FFirstUnknownLongestLine);
400         if (FLastUnknownLongestLine < Line2) then
401           Line2 := Max(fIndexOfLongestLine, FLastUnknownLongestLine);
402       end;
403     end;
404   end;
405 
406   FFirstUnknownLongestLine := -1;
407   FLastUnknownLongestLine := -1;
408 
409   try
410     //Result := 0;
411     m := 0;
412     CharWidths := nil;
413     for i := Line1 to Line2 do begin
414       j := FTabData[i];
415       if j = LINE_LEN_UNKNOWN then begin
416         // embedd a copy of ExpandedStringLength
417         // allows one to re-use CharWidths
418         Line := NextLines.GetPChar(i,LineLen); // fSynStrings[i];
419         j := 0;
420         if (LineLen = 0) then begin
421           FTabData[i] := j + NO_TAB_IN_LINE_OFFSET;
422         end else begin
423           if LineLen > m then begin
424             ReAllocMem(CharWidths, LineLen * SizeOf(TPhysicalCharWidth));
425             m := LineLen;
426           end;
427           DoGetPhysicalCharWidths(Line, LineLen, i, CharWidths);
428           j := FLastLinePhysLen;
429 
430           if j > MAX_LINE_LEN_STORED then
431             FTabData[i] := LINE_LEN_UNKNOWN
432           else if FLastLineHasTab then // FLastLineHasTab is set by GetPhysicalCharWidths
433             FTabData[i] := j
434           else
435             FTabData[i] := j + NO_TAB_IN_LINE_OFFSET;
436         end;
437       end
438       else
439       if j >= NO_TAB_IN_LINE_OFFSET then
440         j := j -  NO_TAB_IN_LINE_OFFSET;
441       if j > Result then begin
442         Result := j;
443         fIndexOfLongestLine := i;
444       end;
445     end;
446   finally
447     ReAllocMem(CharWidths, 0);
448   end;
449 end;
450 
451 end.
452 
453