1 {
2 ***************************************************************************
3 * *
4 * This source is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This code is distributed in the hope that it will be useful, but *
10 * WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
12 * General Public License for more details. *
13 * *
14 * A copy of the GNU General Public License is available on the World *
15 * Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also *
16 * obtain it by writing to the Free Software Foundation, *
17 * Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. *
18 * *
19 ***************************************************************************
20
21 Author: Mattias Gaertner
22
23 Abstract:
24 Source Editor marks for (compiler) messages.
25 }
26 unit etSrcEditMarks;
27
28 {$mode objfpc}{$H+}
29
30 {off $DEFINE VerboseETSrcChange}
31
32 interface
33
34 uses
35 Classes, SysUtils, math, Laz_AVL_Tree,
36 // LCL
37 Graphics, Controls, Forms, ImgList,
38 // LazUtils
39 LazLoggerBase, LazFileUtils,
40 // Codetools
41 KeywordFuncLists,
42 // SynEdit
43 SynEditMarkupGutterMark,
44 SynEditMarks, SynEditMiscClasses, SynEditTypes, SynEdit, LazSynEditText,
45 // IdeIntf
46 IDEExternToolIntf;
47
48 type
49
50 { TETMarkStyle }
51
52 TETMarkStyle = class
53 private
54 FColor: TColor;
55 FImageIndex: integer;
56 FSourceMarkup: TSynSelectedColor;
57 FUrgency: TMessageLineUrgency;
58 procedure SetColor(AValue: TColor);
59 public
60 constructor Create(TheUrgency: TMessageLineUrgency; TheColor: TColor);
61 destructor Destroy; override;
62 property Urgency: TMessageLineUrgency read FUrgency;
63 property Color: TColor read FColor write SetColor;
64 property ImageIndex: integer read FImageIndex write FImageIndex;
65 property SourceMarkup: TSynSelectedColor read FSourceMarkup;
66 end;
67
68 TETMarks = class;
69
70 { TETMark }
71
72 TETMark = class(TSynEditMarkupMark)
73 private
74 FMsgLine: TMessageLine;
75 FSourceMarks: TETMarks;
76 FUrgency: TMessageLineUrgency;
77 public
78 destructor Destroy; override;
79 property Urgency: TMessageLineUrgency read FUrgency write FUrgency;
80 property MsgLine: TMessageLine read FMsgLine write FMsgLine;
81 property SourceMarks: TETMarks read FSourceMarks write FSourceMarks;
82 end;
83
84 { TLMsgViewLine }
85
86 TLMsgViewLine = class(TMessageLine)
87 public
88 Mark: TETMark;
89 destructor Destroy; override;
90 end;
91
92 { TETMarks }
93
94 TOnGetSynEditOfFile = procedure(Sender: TObject; aFilename: string;
95 var aSynEdit: TSynEdit) of object;
96
97 TETMarks = class(TComponent)
98 private
99 FImageList: TCustomImageList;
100 fMarkStyles: array[TMessageLineUrgency] of TETMarkStyle;
101 FOnGetSynEditOfFile: TOnGetSynEditOfFile;
102 FPriority: integer;
GetMarkStylesnull103 function GetMarkStyles(Urgency: TMessageLineUrgency): TETMarkStyle; inline;
104 public
105 constructor Create(AOwner: TComponent); override;
106 destructor Destroy; override;
CreateMarknull107 function CreateMark(MsgLine: TMessageLine; aSynEdit: TSynEdit = nil): TETMark;
108 procedure RemoveMarks(aSynEdit: TSynEdit);
109 property ImageList: TCustomImageList read FImageList write FImageList; // must have same Width/Height as the TSynEdits bookmarkimages
110 property OnGetSynEditOfFile: TOnGetSynEditOfFile read FOnGetSynEditOfFile write FOnGetSynEditOfFile;
111 property MarkStyles[Urgency: TMessageLineUrgency]: TETMarkStyle read GetMarkStyles;
112 property Priority: integer read FPriority write FPriority;
113 end;
114
115 var
116 ExtToolsMarks: TETMarks = nil;
117
118 type
119 TETSrcChangeAction = (
120 etscaInsert,
121 etscaDelete
122 );
123
124 { TETSrcChange }
125
126 TETSrcChange = class
127 public
128 Action: TETSrcChangeAction;
129 FromPos: TPoint;
130 ToPos: TPoint;
131 Prev, Next: TETSrcChange;
132 constructor Create(AnAction: TETSrcChangeAction; const aFromPos, aToPos: TPoint);
133 constructor Create(AnAction: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer);
AsStringnull134 function AsString: string;
135 end;
136
137 TETMultiSrcChanges = class;
138
139 { TETSingleSrcChanges - edits of single file}
140
141 TETSingleSrcChanges = class
142 private
143 FFilename: string;
144 FFirst: TETSrcChange;
145 FLast: TETSrcChange;
146 fInPendingTree: boolean;
147 FMultiSrcChanges: TETMultiSrcChanges;
148 procedure Append(Change: TETSrcChange);
149 procedure Remove(Change: TETSrcChange);
150 procedure SetFilename(AValue: string);
151 procedure SetMultiSrcChanges(AValue: TETMultiSrcChanges);
152 procedure SetInPendingTree(AValue: boolean);
153 protected
154 property InPendingTree: boolean read FInPendingTree write SetInPendingTree;
155 public
156 constructor Create;
157 destructor Destroy; override;
158 procedure Clear;
159 property MultiSrcChanges: TETMultiSrcChanges read FMultiSrcChanges write SetMultiSrcChanges;
160 property First: TETSrcChange read FFirst;
161 property Last: TETSrcChange read FLast;
162 property Filename: string read FFilename write SetFilename;
163 procedure GetRange(out MinY, MaxY, LineDiffBehindMaxY: integer);
Addnull164 function Add(Action: TETSrcChangeAction; const FromPos, ToPos: TPoint): TETSrcChange; inline;
Addnull165 function Add(Action: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer): TETSrcChange;
AdaptCaretnull166 function AdaptCaret(var Line,Col: integer;
167 LeftBound: boolean // true = position is bound to character on the left
168 ): boolean; // true if changed
169 procedure ConsistencyCheck;
170 procedure WriteDebugReport(Title: string);
171 end;
172
173 { TETMultiSrcChanges - edits of all files }
174
175 TETMultiSrcChanges = class(TComponent)
176 private
177 fAllChanges: TAvlTree; // tree of TETSingleSrcChanges sorted for Filename
178 FAutoSync: boolean;
179 fPendingChanges: TAvlTree; // tree of TETSingleSrcChanges sorted for Filename
180 FOnSync: TNotifyEvent;
181 FSyncQueued: boolean;
182 procedure SetSyncQueued(AValue: boolean);
183 protected
184 procedure DoSync({%H-}Data: PtrInt); // called by Application.QueueAsyncCall
185 public
186 constructor Create(AOwner: TComponent); override;
187 destructor Destroy; override;
Countnull188 function Count: integer; inline;
189 procedure Clear;
GetChangesnull190 function GetChanges(const aFilename: string; CreateIfNotExists: boolean): TETSingleSrcChanges;
AdaptCaretnull191 function AdaptCaret(const aFilename: string; var Line,Col: integer;
192 LeftBound: boolean // true = position is bound to character on the left
193 ): boolean;
194 property AllChanges: TAvlTree read fAllChanges; // tree of TETSingleSrcChanges sorted for Filename
195 property PendingChanges: TAvlTree read fPendingChanges; // tree of TETSingleSrcChanges sorted for Filename
196 property SyncQueued: boolean read FSyncQueued write SetSyncQueued;
197 property OnSync: TNotifyEvent read FOnSync write FOnSync; // called by Application.QueueAsyncCall
198 property AutoSync: boolean read FAutoSync write FAutoSync; // true = call OnSync via Application.QueueAsyncCall
199 end;
200
201 { TETSynPlugin - create one per file, not one per synedit }
202
endernull203 TIsEnabledEvent = function(Sender: TObject): boolean of object;
204
205 TETSynPlugin = class(TLazSynEditPlugin)
206 private
207 FChanges: TETSingleSrcChanges;
208 FEnabled: boolean;
209 FOnIsEnabled: TIsEnabledEvent;
210 protected
211 procedure OnLineEdit(Sender: TSynEditStrings; aLinePos, aBytePos, aCount,
212 aLineBrkCnt: Integer; {%H-}aText: String);
213 public
214 constructor Create(AOwner: TComponent); override;
215 destructor Destroy; override;
216 property Changes: TETSingleSrcChanges read FChanges write FChanges;
217 property Enabled: boolean read FEnabled write FEnabled;
218 property OnIsEnabled: TIsEnabledEvent read FOnIsEnabled write FOnIsEnabled;
219 end;
220
IsCaretInFrontnull221 function IsCaretInFront(Line1, Col1, Line2, Col2: integer): boolean; inline; overload;
IsCaretInFrontnull222 function IsCaretInFront(const P1: TPoint; Line2, Col2: integer): boolean; inline; overload;
IsCaretInFrontnull223 function IsCaretInFront(Line1, Col1: integer; const P2: TPoint): boolean; inline; overload;
IsCaretInFrontnull224 function IsCaretInFront(const P1,P2: TPoint): boolean; inline; overload;
IsCaretInFrontOrSamenull225 function IsCaretInFrontOrSame(Line1, Col1, Line2, Col2: integer): boolean; inline; overload;
IsCaretInFrontOrSamenull226 function IsCaretInFrontOrSame(const P1: TPoint; Line2, Col2: integer): boolean; inline; overload;
IsCaretInFrontOrSamenull227 function IsCaretInFrontOrSame(Line1, Col1: integer; const P2: TPoint): boolean; inline; overload;
IsCaretInFrontOrSamenull228 function IsCaretInFrontOrSame(const P1,P2: TPoint): boolean; inline; overload;
229 procedure AdaptCaret(var Line,Col: integer;
230 LeftBound: boolean; // true = position is bound to character on the left
231 Action: TETSrcChangeAction;
232 FromPosY, FromPosX, ToPosY, ToPosX: integer);
233
CompareETSrcChangesFilenamesnull234 function CompareETSrcChangesFilenames(Changes1, Changes2: Pointer): integer;
CompareFilenameAndETSrcChangesnull235 function CompareFilenameAndETSrcChanges(aFilenameStr, Changes: Pointer): integer;
236
dbgsnull237 function dbgs(Action: TETSrcChangeAction): string; overload;
238 procedure Test_AdaptCaret;
239 procedure Test_MergeTETSrcChanges;
240
241 implementation
242
IsCaretInFrontnull243 function IsCaretInFront(Line1, Col1, Line2, Col2: integer): boolean;
244 begin
245 Result:=(Line1<Line2) or ((Line1=Line2) and (Col1<Col2));
246 end;
247
IsCaretInFrontnull248 function IsCaretInFront(const P1: TPoint; Line2, Col2: integer): boolean;
249 begin
250 Result:=IsCaretInFront(P1.Y,P1.X,Line2,Col2);
251 end;
252
IsCaretInFrontnull253 function IsCaretInFront(Line1, Col1: integer; const P2: TPoint): boolean;
254 begin
255 Result:=IsCaretInFront(Line1,Col1,P2.Y,P2.X);
256 end;
257
IsCaretInFrontnull258 function IsCaretInFront(const P1, P2: TPoint): boolean;
259 begin
260 Result:=IsCaretInFront(P1.Y,P1.X,P2.Y,P2.X);
261 end;
262
IsCaretInFrontOrSamenull263 function IsCaretInFrontOrSame(Line1, Col1, Line2, Col2: integer): boolean;
264 begin
265 Result:=(Line1<Line2) or ((Line1=Line2) and (Col1<=Col2));
266 end;
267
IsCaretInFrontOrSamenull268 function IsCaretInFrontOrSame(const P1: TPoint; Line2, Col2: integer): boolean;
269 begin
270 Result:=IsCaretInFrontOrSame(P1.Y,P1.X,Line2,Col2);
271 end;
272
IsCaretInFrontOrSamenull273 function IsCaretInFrontOrSame(Line1, Col1: integer; const P2: TPoint): boolean;
274 begin
275 Result:=IsCaretInFrontOrSame(Line1,Col1,P2.Y,P2.X);
276 end;
277
IsCaretInFrontOrSamenull278 function IsCaretInFrontOrSame(const P1, P2: TPoint): boolean;
279 begin
280 Result:=IsCaretInFrontOrSame(P1.Y,P1.X,P2.Y,P2.X);
281 end;
282
283 procedure AdaptCaret(var Line, Col: integer; LeftBound: boolean;
284 Action: TETSrcChangeAction; FromPosY, FromPosX, ToPosY, ToPosX: integer);
285 begin
286 //debugln(['AdaptCaret Line=',Line,' Col=',Col,' LeftBound=',LeftBound,' Action=',dbgs(Action),' FromPos=',FromPosY,',',FromPosX,' ToPos=',ToPosY,',',ToPosX]);
287 if Line<FromPosY then exit;
288 if Action=etscaInsert then begin
289 // insert
290 if Line>FromPosY then begin
291 // insert in lines in front => move vertically
292 inc(Line,ToPosY-FromPosY);
293 end else begin
294 // insert in same line
295 if LeftBound then begin
296 if Col<=FromPosX then exit;
297 end else begin
298 if Col<FromPosX then exit;
299 end;
300 if FromPosY<ToPosY then begin
301 // multi line insert
302 inc(Line,ToPosY-FromPosY);
303 Col:=ToPosX+Col-FromPosX;
304 end else begin
305 // inserting some characters
306 inc(Col,ToPosX-FromPosX);
307 end;
308 end;
309 end else begin
310 // delete
311 if Line>ToPosY then begin
312 // delete some lines in front => move vertically
313 dec(Line,ToPosY-FromPosY);
314 end else if Line<ToPosY then begin
315 if Line>FromPosY then begin
316 // whole line of position was deleted => move to start of deletion
317 Line:=FromPosY;
318 Col:=FromPosX;
319 end else begin
320 // Line=FromPosY, Line<ToPosY
321 if Col<=FromPosX then begin
322 // delete is behind position => ignore
323 end else begin
324 // position was deleted => move to start of deletion
325 Line:=FromPosY;
326 Col:=FromPosX;
327 end;
328 end;
329 end else begin
330 // Line=ToPosY
331 if Line>FromPosY then begin
332 // multi line delete
333 if Col<=ToPosX then begin
334 // position was deleted => move to start of deletion
335 Line:=FromPosY;
336 Col:=FromPosX;
337 end else begin
338 // some characters at the start of the line were deleted
339 Line:=FromPosY;
340 dec(Col,ToPosX-1);
341 end;
342 end else begin
343 // Line=FromPosY=ToPosY
344 if Col<=FromPosX then begin
345 // delete is behind position => ignore
346 end else if Col<=ToPosX then begin
347 // position was deleted => move to start of deletion
348 Col:=FromPosX;
349 end else begin
350 // some characters in front were deleted
351 dec(Col,ToPosX-FromPosX);
352 end;
353 end;
354 end;
355 end;
356 //debugln(['AdaptCaret ',Line,',',Col]);
357 end;
358
CompareETSrcChangesFilenamesnull359 function CompareETSrcChangesFilenames(Changes1, Changes2: Pointer): integer;
360 var
361 SrcChanges1: TETSingleSrcChanges absolute Changes1;
362 SrcChanges2: TETSingleSrcChanges absolute Changes2;
363 begin
364 Result:=CompareFilenames(SrcChanges1.Filename,SrcChanges2.Filename);
365 end;
366
CompareFilenameAndETSrcChangesnull367 function CompareFilenameAndETSrcChanges(aFilenameStr, Changes: Pointer
368 ): integer;
369 var
370 SrcChanges: TETSingleSrcChanges absolute Changes;
371 begin
372 Result:=CompareFilenames(AnsiString(aFilenameStr),SrcChanges.Filename);
373 end;
374
dbgsnull375 function dbgs(Action: TETSrcChangeAction): string;
376 begin
377 Result:='';
378 WriteStr(Result,Action);
379 end;
380
381 procedure Test_AdaptCaret;
382
383 procedure T(Title: string; Line,Col: integer;
384 LeftBound: boolean; // true = position is bound to character on the left
385 Action: TETSrcChangeAction;
386 FromPosY, FromPosX, ToPosY, ToPosX: integer;
387 ExpectedLine, ExpectedCol: integer);
388 var
389 Y: Integer;
390 X: Integer;
391 s: String;
392 begin
393 Y:=Line;
394 X:=Col;
395 AdaptCaret(Y,X,LeftBound,Action,FromPosY,FromPosX,ToPosY,ToPosX);
396 if (Y=ExpectedLine) and (X=ExpectedCol) then exit;
397 s:='Test_AdaptCaret: Caret='+dbgs(Line)+','+dbgs(Col)
398 +' LeftBound='+dbgs(LeftBound)
399 +' Action='+dbgs(Action)
400 +' FromPos='+dbgs(FromPosY)+','+dbgs(FromPosX)
401 +' ToPos='+dbgs(ToPosY)+','+dbgs(ToPosX)
402 +' Expected='+dbgs(ExpectedLine)+','+dbgs(ExpectedCol)
403 +' Actual='+dbgs(Y)+','+dbgs(X);
404 raise Exception.Create(Title+': '+s);
405 end;
406
407 begin
408 T('Insert chars in front',10,10,true,etscaInsert,1,1, 1,2, 10,10);
409 T('Insert lines in front',10,10,true,etscaInsert,1,1, 2,2, 11,10);
410 T('Insert chars behind',10,10,true,etscaInsert,12,1, 12,2, 10,10);
411 T('Insert chars in front, same line',10,10,true,etscaInsert,10,1, 10,2, 10,11);
412 T('Insert chars in front, same line',10,40,true,etscaInsert,10,28, 10,29, 10,41);
413 T('Insert chars behind, same line',10,10,true,etscaInsert,10,11, 10,12, 10,10);
414 T('Insert chars behind, same line, leftbound',10,10,true,etscaInsert,10,10, 10,12, 10,10);
415 T('Insert chars behind, same line, rightbound',10,10,false,etscaInsert,10,10, 10,12, 10,12);
416 T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,1, 11,1, 11,10);
417 T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,1, 11,2, 11,11);
418 T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 11,2, 11,10);
419 T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 11,5, 11,13);
420 T('Insert chars and line breaks in front, same line',10,10,true,etscaInsert,10,2, 13,5, 13,13);
421
422 T('Delete chars in front',10,10,true,etscaDelete, 1,1, 1,2, 10,10);
423 T('Delete lines in front',10,10,true,etscaDelete, 1,1, 2,2, 9,10);
424 T('Delete chars in front, same line',10,10,true,etscaDelete, 10,1, 10,2, 10,9);
425 T('Delete lines behind',10,10,true,etscaDelete, 11,1, 12,2, 10,10);
426 T('Delete chars behind, same line',10,10,true,etscaDelete, 10,11, 10,12, 10,10);
427 T('Delete chars behind, same line',10,10,true,etscaDelete, 10,10, 10,12, 10,10);
428 T('Delete lines in front, same line',10,10,true,etscaDelete, 9,1, 10,1, 9,10);
429 T('Delete lines in front, same line',10,10,true,etscaDelete, 9,1, 10,3, 9,8);
430 T('Delete position',10,10,true,etscaDelete, 9,1, 11,1, 9,1);
431 T('Delete position',10,10,true,etscaDelete, 10,1, 11,1, 10,1);
432 T('Delete position',10,10,true,etscaDelete, 10,5, 10,11, 10,5);
433 end;
434
435 procedure Test_MergeTETSrcChanges;
436 var
437 Changes: TETSingleSrcChanges;
438
439 procedure Check(Title: string; const aChanges: array of TETSrcChange);
440
441 procedure E(Msg: string);
442 var
443 s: String;
444 begin
445 s:=Title+', '+Msg;
446 Changes.WriteDebugReport(s);
447 raise Exception.Create(s);
448 end;
449
450 var
451 i: Integer;
452 ActualChange: TETSrcChange;
453 ExpectedChange: TETSrcChange;
454 begin
455 ActualChange:=Changes.First;
456 try
457 for i:=Low(aChanges) to High(aChanges) do begin
458 ExpectedChange:=aChanges[i];
459 if ExpectedChange=nil then begin
460 if ActualChange<>nil then
461 E('too many changes');
462 exit;
463 end;
464 if ActualChange=nil then
465 E('not enough changes (missing: '+ActualChange.AsString+')');
466 if ExpectedChange.AsString<>ActualChange.AsString then
467 E('diff: Expected=('+ExpectedChange.AsString+'), Actual=('+ActualChange.AsString+')');
468 ActualChange:=ActualChange.Next;
469 end;
470 finally
471 for i:=Low(aChanges) to High(aChanges) do
472 aChanges[i].Free;
473 end;
474 end;
475
476 begin
477 Changes:=TETSingleSrcChanges.Create;
478 try
479 Changes.ConsistencyCheck;
480
481 // test empty clear
482 Changes.Clear;
483 Changes.ConsistencyCheck;
484
485 // test merge insert
486 Changes.Add(etscaInsert,1,1,1,46);
487 Changes.ConsistencyCheck;
488 Changes.Add(etscaInsert,1,46,2,1);
489 Changes.ConsistencyCheck;
490 Check('Merge insert',[TETSrcChange.Create(etscaInsert,1,1,2,1)]);
491 Changes.Clear;
492
493 // insert characters into a previous multi line insert
494 Changes.Add(etscaInsert,10,1,12,1);
495 Changes.ConsistencyCheck;
496 Changes.Add(etscaInsert,10,1,10,2);
497 Changes.ConsistencyCheck;
498 Check('Ignore small insert',[TETSrcChange.Create(etscaInsert,10,1,12,1)]);
499 Changes.Clear;
500
501 // delete behind previous delete
502 Changes.Add(etscaDelete,1,2,1,4);
503 Changes.ConsistencyCheck;
504 Changes.Add(etscaDelete,1,2,1,5);
505 Changes.ConsistencyCheck;
506 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,2,1,7)]);
507 Changes.Clear;
508
509 // delete encloses a previous delete
510 Changes.Add(etscaDelete,1,2,1,4);
511 Changes.ConsistencyCheck;
512 Changes.Add(etscaDelete,1,1,1,5);
513 Changes.ConsistencyCheck;
514 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,1,7)]);
515 Changes.Clear;
516
517 // delete in front of a previous delete
518 Changes.Add(etscaDelete,2,2,2,4);
519 Changes.ConsistencyCheck;
520 Changes.Add(etscaDelete,1,1,2,2);
521 Changes.ConsistencyCheck;
522 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,2,4)]);
523 Changes.Clear;
524
525 // delete encloses a previous delete of characters
526 Changes.Add(etscaDelete,2,2,2,4);
527 Changes.ConsistencyCheck;
528 Changes.Add(etscaDelete,1,1,3,1);
529 Changes.ConsistencyCheck;
530 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,1)]);
531 Changes.Clear;
532
533 // delete encloses a previous delete of a line
534 Changes.Add(etscaDelete,2,2,3,4);
535 Changes.ConsistencyCheck;
536 Changes.Add(etscaDelete,1,1,4,1);
537 Changes.ConsistencyCheck;
538 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,5,1)]);
539 Changes.Clear;
540
541 // delete encloses a previous delete at end
542 Changes.Add(etscaDelete,2,2,3,1);
543 Changes.ConsistencyCheck;
544 Changes.Add(etscaDelete,1,1,2,3);
545 Changes.ConsistencyCheck;
546 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,2)]);
547 Changes.Clear;
548
549 // delete encloses a previous delete at end
550 Changes.Add(etscaDelete,2,2,3,2);
551 Changes.ConsistencyCheck;
552 Changes.Add(etscaDelete,1,1,2,3);
553 Changes.ConsistencyCheck;
554 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,3)]);
555 Changes.Clear;
556
557 // delete encloses a previous delete at start
558 Changes.Add(etscaDelete,1,2,3,2);
559 Changes.ConsistencyCheck;
560 Changes.Add(etscaDelete,1,1,1,2);
561 Changes.ConsistencyCheck;
562 Check('combine deleting characters',[TETSrcChange.Create(etscaDelete,1,1,3,2)]);
563 Changes.Clear;
564 finally
565 Changes.Free;
566 end;
567 end;
568
569 { TETMultiSrcChanges }
570
571 // inline
TETMultiSrcChanges.Countnull572 function TETMultiSrcChanges.Count: integer;
573 begin
574 Result:=fAllChanges.Count;
575 end;
576
577 procedure TETMultiSrcChanges.SetSyncQueued(AValue: boolean);
578 begin
579 //debugln(['TETMultiSrcChanges.SetSyncQueued ',AValue]);
580 if csDesigning in ComponentState then
581 AValue:=false;
582 if FSyncQueued=AValue then Exit;
583 FSyncQueued:=AValue;
584 if SyncQueued then
585 Application.QueueAsyncCall(@DoSync,0)
586 else
587 Application.RemoveAsyncCalls(Self);
588 end;
589
590 procedure TETMultiSrcChanges.DoSync(Data: PtrInt);
591 var
592 Node, NextNode: TAvlTreeNode;
593 begin
594 //debugln(['TETMultiSrcChanges.DoSync Files=',fPendingChanges.Count]);
595 FSyncQueued:=false;
596 if fPendingChanges.Count=0 then exit;
597 if Assigned(OnSync) then
598 OnSync(Self);
599 // clear pending
600 Node:=fPendingChanges.FindLowest;
601 while Node<>nil do begin
602 NextNode:=Node.Successor;
603 TETSingleSrcChanges(Node.Data).Clear; // this removes Node from fPendingChanges
604 Node:=NextNode;
605 end;
606 fPendingChanges.Clear;
607 //debugln(['TETMultiSrcChanges.DoSync END FSyncQueued=',FSyncQueued,' fPendingChanges=',fPendingChanges.Count]);
608 end;
609
610 constructor TETMultiSrcChanges.Create(AOwner: TComponent);
611 begin
612 inherited Create(AOwner);
613 fAllChanges:=TAvlTree.Create(@CompareETSrcChangesFilenames);
614 fPendingChanges:=TAvlTree.Create(@CompareETSrcChangesFilenames);
615 end;
616
617 destructor TETMultiSrcChanges.Destroy;
618 begin
619 SyncQueued:=false;
620 Clear;
621 FreeAndNil(fPendingChanges);
622 FreeAndNil(fAllChanges);
623 inherited Destroy;
624 end;
625
626 procedure TETMultiSrcChanges.Clear;
627 begin
628 SyncQueued:=false;
629 fPendingChanges.Clear;
630 fAllChanges.FreeAndClear;
631 end;
632
GetChangesnull633 function TETMultiSrcChanges.GetChanges(const aFilename: string;
634 CreateIfNotExists: boolean): TETSingleSrcChanges;
635 var
636 Node: TAvlTreeNode;
637 begin
638 Node:=fAllChanges.FindKey(Pointer(aFilename),@CompareFilenameAndETSrcChanges);
639 if Node<>nil then
640 Result:=TETSingleSrcChanges(Node.Data)
641 else if CreateIfNotExists then begin
642 Result:=TETSingleSrcChanges.Create;
643 Result.Filename:=aFilename;
644 Result.MultiSrcChanges:=Self;
645 end else
646 Result:=nil;
647 end;
648
TETMultiSrcChanges.AdaptCaretnull649 function TETMultiSrcChanges.AdaptCaret(const aFilename: string; var Line,
650 Col: integer; LeftBound: boolean): boolean;
651 var
652 Changes: TETSingleSrcChanges;
653 begin
654 Changes:=GetChanges(aFilename,false);
655 if Changes=nil then
656 Result:=false
657 else
658 Result:=Changes.AdaptCaret(Line,Col,LeftBound);
659 end;
660
661 { TETSrcChange }
662
663 constructor TETSrcChange.Create(AnAction: TETSrcChangeAction; const aFromPos,
664 aToPos: TPoint);
665 begin
666 Action:=AnAction;
667 FromPos:=aFromPos;
668 ToPos:=aToPos;
669 end;
670
671 constructor TETSrcChange.Create(AnAction: TETSrcChangeAction; FromPosY,
672 FromPosX, ToPosY, ToPosX: integer);
673 begin
674 Action:=AnAction;
675 FromPos.Y:=FromPosY;
676 FromPos.X:=FromPosX;
677 ToPos.Y:=ToPosY;
678 ToPos.X:=ToPosX;
679 end;
680
AsStringnull681 function TETSrcChange.AsString: string;
682 begin
683 if Action=etscaInsert then
684 Result:='Insert'
685 else
686 Result:='Delete';
687 Result+='-From='+IntToStr(FromPos.Y)+','+IntToStr(FromPos.X);
688 Result+='-To='+IntToStr(ToPos.Y)+','+IntToStr(ToPos.X);
689 end;
690
691 { TETSingleSrcChanges }
692
693 procedure TETSingleSrcChanges.SetInPendingTree(AValue: boolean);
694 begin
695 if FMultiSrcChanges=nil then AValue:=false;
696 if FInPendingTree=AValue then Exit;
697 FInPendingTree:=AValue;
698 if fInPendingTree then begin
699 FMultiSrcChanges.fPendingChanges.Add(Self);
700 if FMultiSrcChanges.AutoSync then
701 FMultiSrcChanges.SyncQueued:=true;
702 end else
703 FMultiSrcChanges.fPendingChanges.Remove(Self);
704 end;
705
706 procedure TETSingleSrcChanges.Append(Change: TETSrcChange);
707 begin
708 if First=nil then begin
709 FFirst:=Change;
710 InPendingTree:=true;
711 end else begin
712 FLast.Next:=Change;
713 Change.Prev:=Last;
714 end;
715 fLast:=Change;
716 end;
717
718 procedure TETSingleSrcChanges.Remove(Change: TETSrcChange);
719 begin
720 if First=Change then begin
721 FFirst:=Change.Next;
722 if FFirst=nil then
723 InPendingTree:=false;
724 end;
725 if Last=Change then
726 fLast:=Change.Prev;
727 if Change.Prev<>nil then
728 Change.Prev.Next:=Change.Next;
729 if Change.Next<>nil then
730 Change.Next.Prev:=Change.Prev;
731 Change.Prev:=nil;
732 Change.Next:=nil;
733 end;
734
735 procedure TETSingleSrcChanges.SetFilename(AValue: string);
736 var
737 HasChanged: Boolean;
738 begin
739 if FFilename=AValue then Exit;
740 HasChanged:=CompareFilenames(FFilename,AValue)<>0;
741 if HasChanged then begin
742 if FMultiSrcChanges<>nil then
743 raise Exception.Create('TETSingleSrcChanges.SetFilename');
744 end;
745 FFilename:=AValue;
746 if HasChanged then
747 Clear;
748 end;
749
750 procedure TETSingleSrcChanges.SetMultiSrcChanges(AValue: TETMultiSrcChanges);
751 begin
752 if FMultiSrcChanges=AValue then Exit;
753 if Filename='' then
754 raise Exception.Create('TETSingleSrcChanges.SetMultiSrcChanges empty filename');
755 if (FMultiSrcChanges<>nil) then begin
756 if (csDestroying in FMultiSrcChanges.ComponentState) then begin
757 fInPendingTree:=false;
758 end else begin
759 InPendingTree:=false;
760 FMultiSrcChanges.fAllChanges.Remove(Self);
761 end;
762 end;
763 FMultiSrcChanges:=AValue;
764 if FMultiSrcChanges<>nil then begin
765 FMultiSrcChanges.fAllChanges.Add(Self);
766 InPendingTree:=First<>nil;
767 end;
768 end;
769
770 constructor TETSingleSrcChanges.Create;
771 begin
772 end;
773
774 destructor TETSingleSrcChanges.Destroy;
775 begin
776 MultiSrcChanges:=nil;
777 Clear;
778 inherited Destroy;
779 end;
780
781 procedure TETSingleSrcChanges.Clear;
782 var
783 Item: TETSrcChange;
784 CurItem: TETSrcChange;
785 begin
786 Item:=First;
787 while Item<>nil do begin
788 CurItem:=Item;
789 Item:=Item.Next;
790 CurItem.Free;
791 end;
792 fFirst:=nil;
793 FLast:=nil;
794 InPendingTree:=false;
795 end;
796
797 procedure TETSingleSrcChanges.GetRange(out MinY, MaxY, LineDiffBehindMaxY: integer);
798 // true if there are changes
799 // All changes were done between lines MinY and MaxY (inclusive).
800 // Lines behind MaxY are moved by LineDiffBehindMaxY.
801 // In other words:
802 // if MinY<=Line<=MaxY then AdaptCaret(Line,Col,...)
803 // else if Line>MaxY then inc(Line,LineDiffBehindMaxY);
804 var
805 Change: TETSrcChange;
806 y: Integer;
807 x: Integer;
808 begin
809 MinY:=High(Integer);
810 MaxY:=0;
811 LineDiffBehindMaxY:=0;
812 Change:=First;
813 if Change=nil then exit;
814 while Change<>nil do begin
815 MinY:=Min(MinY,Change.FromPos.Y);
816 if Change.Action=etscaInsert then
817 MaxY:=Max(MaxY,Change.FromPos.Y)
818 else
819 MaxY:=Max(MaxY,Change.ToPos.Y);
820 Change:=Change.Next;
821 end;
822 y:=MaxY+1;
823 x:=1;
824 AdaptCaret(y,x,true);
825 LineDiffBehindMaxY:=y-(MaxY+1);
826 end;
827
828 // inline
Addnull829 function TETSingleSrcChanges.Add(Action: TETSrcChangeAction; const FromPos,
830 ToPos: TPoint): TETSrcChange;
831 begin
832 Result:=Add(Action,FromPos.Y,FromPos.X,ToPos.Y,ToPos.X);
833 end;
834
Addnull835 function TETSingleSrcChanges.Add(Action: TETSrcChangeAction; FromPosY, FromPosX,
836 ToPosY, ToPosX: integer): TETSrcChange;
837
838 procedure RaiseFromPosBehindToPos;
839 begin
840 raise Exception.CreateFmt('TETSrcChanges.Add FromPos=%s,%s behind ToPos=%s,%s',[FromPosY,FromPosX,ToPosY,ToPosX]);
841 end;
842
Mergenull843 function Merge(Prev, Cur: TETSrcChange): boolean;
844 begin
845 if (Prev=nil) or (Prev.Action<>Action) then
846 exit(false);
847 // check if addition can be merged
848 if Action=etscaInsert then begin
849 // Insertion
850 if (Prev.ToPos.Y=Cur.FromPos.Y) and (Prev.ToPos.X=Cur.FromPos.X) then begin
851 // Cur is an insert exactly behind Prev insert -> append insert
852 Prev.ToPos.Y:=Cur.ToPos.Y;
853 Prev.ToPos.X:=Cur.ToPos.X;
854 {$IFDEF VerboseETSrcChange}
855 debugln(['TETSrcChanges.Add appending insert: ',Prev.AsString]);
856 {$ENDIF}
857 exit(true);
858 end;
859 if (Cur.FromPos.Y=Cur.ToPos.Y)
860 and (Prev.FromPos.Y<=Cur.FromPos.Y) and (Prev.ToPos.Y>Cur.FromPos.Y) then begin
861 // Cur inserts characters into a Prev multi line insert -> ignore
862 {$IFDEF VerboseETSrcChange}
863 debugln(['TETSrcChanges.Add inserting characters into a multi line insert -> ignore']);
864 {$ENDIF}
865 exit(true);
866 end;
867 // ToDo: insert exactly in front
868 end else begin
869 // Deletion
870 if IsCaretInFrontOrSame(Cur.FromPos,Prev.FromPos)
871 and IsCaretInFrontOrSame(Prev.FromPos,Cur.ToPos) then begin
872 // Cur delete extends Prev delete => combine delete
873 etSrcEditMarks.AdaptCaret(Cur.ToPos.Y,Cur.ToPos.X,false,etscaInsert,
874 Prev.FromPos.Y,Prev.FromPos.X,Prev.ToPos.Y,Prev.ToPos.X);
875 Prev.ToPos:=Cur.ToPos;
876 Prev.FromPos:=Cur.FromPos;
877 {$IFDEF VerboseETSrcChange}
878 debugln(['TETSrcChanges.Add delete encloses previous delete: ',Prev.AsString]);
879 {$ENDIF}
880 exit(true);
881 end;
882 end;
883 Result:=false;
884 end;
885
886 begin
887 {$IFDEF VerboseETSrcChange}
888 debugln(['TETSrcChanges.Add Action=',dbgs(Action),' From=',FromPosY,',',FromPosX,' To=',ToPosY,',',ToPosX]);
889 {$ENDIF}
890
891 if (FromPosY=ToPosY) and (FromPosX=ToPosX) then
892 exit(nil); // no change => ignore
893
894 // consistency check
895 if IsCaretInFront(ToPosY,ToPosX,FromPosY,FromPosX) then
896 RaiseFromPosBehindToPos;
897
898 Result:=TETSrcChange.Create(Action, FromPosY, FromPosX, ToPosY, ToPosX);
899
900 if Merge(Last,Result) then begin
901 repeat
902 Result.Free;
903 Result:=Last;
904 if not Merge(Result.Prev,Result) then exit;
905 Remove(Last);
906 until false;
907 end else begin
908 Append(Result);
909 end;
910 end;
911
TETSingleSrcChanges.AdaptCaretnull912 function TETSingleSrcChanges.AdaptCaret(var Line, Col: integer; LeftBound: boolean
913 ): boolean;
914 var
915 Change: TETSrcChange;
916 OldCol: Integer;
917 OldLine: Integer;
918 begin
919 OldCol:=Col;
920 OldLine:=Line;
921 Change:=First;
922 while Change<>nil do begin
923 etSrcEditMarks.AdaptCaret(Line,Col,LeftBound,Change.Action,
924 Change.FromPos.Y,Change.FromPos.X,Change.ToPos.Y,Change.ToPos.X);
925 Change:=Change.Next;
926 end;
927 Result:=(Line<>OldLine) or (Col<>OldCol);
928 end;
929
930 procedure TETSingleSrcChanges.ConsistencyCheck;
931
932 procedure E(Msg: string);
933 begin
934 raise Exception.Create('TETSrcChanges ConsistencyError: '+Msg);
935 end;
936
937 var
938 Change: TETSrcChange;
939 List: TFPList;
940 ReallyInPendingTree: Boolean;
941 begin
942 if (First=nil)<>(Last=nil) then
943 E('(First=nil)<>(Last=nil)');
944 List:=TFPList.Create;
945 try
946 Change:=First;
947 while Change<>nil do begin
948 if IsCaretInFront(Change.ToPos,Change.FromPos) then
949 E('FromPos>ToPos: '+Change.AsString);
950 if Change.Prev<>nil then begin
951 if Change.Prev.Next<>Change then
952 E('Change.Prev.Next<>Change '+Change.AsString);
953 end else begin
954 if Change<>First then
955 E('Change.Prev=nil');
956 end;
957 if (Change.Next=nil) and (Change<>Last) then
958 E('Change.Next=nil');
959 if List.IndexOf(Change)>=0 then
960 E('Cycle '+Change.AsString);
961 List.Add(Change);
962 Change:=Change.Next;
963 end;
964 finally
965 List.Free;
966 end;
967 if MultiSrcChanges<>nil then begin
968 if MultiSrcChanges.fAllChanges.Find(Self)=nil then
969 E('MultiSrcChanges.fAllChanges.Find(Self)=nil');
970 ReallyInPendingTree:=MultiSrcChanges.fPendingChanges.Find(Self)<>nil;
971 if InPendingTree<>ReallyInPendingTree then
972 E('InPendingTree<>ReallyInPendingTree');
973 end else begin
974 if InPendingTree then
975 E('MultiSrcChanges=nil InPendingTree=true');
976 end;
977 end;
978
979 procedure TETSingleSrcChanges.WriteDebugReport(Title: string);
980 var
981 Change: TETSrcChange;
982 begin
983 debugln('TETSrcChanges.WriteDebugReport ',Title);
984 Change:=First;
985 while Change<>nil do begin
986 debugln(' ',Change.AsString);
987 Change:=Change.Next;
988 end;
989 end;
990
991 { TETSynPlugin }
992
993 procedure TETSynPlugin.OnLineEdit(Sender: TSynEditStrings; aLinePos, aBytePos,
994 aCount, aLineBrkCnt: Integer; aText: String);
995 {
996 aLinePos is 1-based
997 aBytePos is 1-based column in line
998
999 Insert:
1000 aCount > 0
1001 Delete:
1002 aCount < 0
1003 Example deleting line 290..292:
1004 LinePos=291 BytePos=1 Count=-45 LineBrkCnt=0 Text=""
1005 LinePos=292 BytePos=1 Count=-33 LineBrkCnt=0 Text=""
1006 LinePos=291 BytePos=1 Count=0 LineBrkCnt=-2 Text=""
1007 LinePos=290 BytePos=70 Count=0 LineBrkCnt=-1 Text=""
1008 LinePos=290 BytePos=1 Count=-69 LineBrkCnt=0 Text=""
1009 }
1010 begin
1011 if not Enabled then exit;
1012 if Changes=nil then exit;
1013 if Assigned(OnIsEnabled) and not OnIsEnabled(Self) then exit;
1014
1015 {$IFDEF VerboseETSrcChange}
1016 debugln(['TETSynPlugin.OnLineEdit LinePos=',aLinePos,' BytePos=',aBytePos,' Count=',aCount,' LineBrkCnt=',aLineBrkCnt,' Text="',dbgstr(aText),'"']);
1017 {$ENDIF}
1018 if aCount>0 then begin
1019 // insert characters
1020 FChanges.Add(etscaInsert,aLinePos,aBytePos,aLinePos,aBytePos+aCount);
1021 end else if aCount<0 then begin
1022 // delete characters
1023 FChanges.Add(etscaDelete,aLinePos,aBytePos,aLinePos,aBytePos-aCount);
1024 end else if aLineBrkCnt>0 then begin
1025 // insert line breaks
1026 // Note: always at end of line, because Count=0
1027 FChanges.Add(etscaInsert,aLinePos,aBytePos,aLinePos+aLineBrkCnt,1);
1028 end else if aLineBrkCnt<0 then begin
1029 // delete line breaks / empty lines
1030 FChanges.Add(etscaDelete,aLinePos,aBytePos,aLinePos-aLineBrkCnt,1);
1031 end;
1032 end;
1033
1034 constructor TETSynPlugin.Create(AOwner: TComponent);
1035 begin
1036 inherited Create(AOwner);
1037 ViewedTextBuffer.AddEditHandler(@OnLineEdit);
1038 FEnabled:=true;
1039 end;
1040
1041 destructor TETSynPlugin.Destroy;
1042 begin
1043 ViewedTextBuffer.RemoveEditHandler(@OnLineEdit);
1044 inherited Destroy;
1045 end;
1046
1047 { TETMark }
1048
1049 destructor TETMark.Destroy;
1050 begin
1051 if MsgLine is TLMsgViewLine then
1052 TLMsgViewLine(MsgLine).Mark:=nil;
1053 MsgLine:=nil;
1054 inherited Destroy;
1055 end;
1056
1057 { TETMarks }
1058
1059 // inline
GetMarkStylesnull1060 function TETMarks.GetMarkStyles(Urgency: TMessageLineUrgency): TETMarkStyle;
1061 begin
1062 Result:=fMarkStyles[Urgency];
1063 end;
1064
1065 constructor TETMarks.Create(AOwner: TComponent);
1066 const
1067 DefMarkColorHint = TColor($00a5ff);
1068 DefMarkColorError = clRed;
1069 var
1070 u: TMessageLineUrgency;
1071 begin
1072 inherited Create(AOwner);
1073 if ExtToolsMarks=nil then
1074 ExtToolsMarks:=Self;
1075 for u:=low(TMessageLineUrgency) to high(TMessageLineUrgency) do
1076 fMarkStyles[u]:=TETMarkStyle.Create(u,DefMarkColorHint);
1077 fMarkStyles[mluWarning].Color:=DefMarkColorError;
1078 fMarkStyles[mluError].Color:=DefMarkColorError;
1079 fMarkStyles[mluFatal].Color:=DefMarkColorError;
1080 fMarkStyles[mluPanic].Color:=DefMarkColorError;
1081 end;
1082
1083 destructor TETMarks.Destroy;
1084 var
1085 u: TMessageLineUrgency;
1086 begin
1087 if ExtToolsMarks=Self then
1088 ExtToolsMarks:=nil;
1089 for u:=low(TMessageLineUrgency) to high(TMessageLineUrgency) do
1090 FreeAndNil(fMarkStyles[u]);
1091 inherited Destroy;
1092 end;
1093
CreateMarknull1094 function TETMarks.CreateMark(MsgLine: TMessageLine; aSynEdit: TSynEdit
1095 ): TETMark;
1096 var
1097 Line: Integer;
1098 Column: Integer;
1099 LineSrc: String;
1100 begin
1101 Result:=nil;
1102 if (MsgLine.Line<1) or (MsgLine.Column<1) or (MsgLine.Filename='') then exit;
1103 if aSynEdit=nil then begin
1104 if OnGetSynEditOfFile=nil then exit;
1105 OnGetSynEditOfFile(Self,MsgLine.Filename,aSynEdit);
1106 if (aSynEdit=nil) then exit;
1107 end;
1108 Line:=MsgLine.Line;
1109 Column:=MsgLine.Column;
1110 if (mlfLeftToken in MsgLine.Flags) then begin
1111 // the mark is at the of the token
1112 // synedit only supports starts of tokens
1113 // => adjust to start of token
1114 if (Column>1) and (Line>=1) and (Line<=aSynEdit.Lines.Count) then begin
1115 LineSrc:=aSynEdit.Lines[Line-1];
1116 if (Column<=length(LineSrc)+1) and (not IsSpaceChar[LineSrc[Column-1]])
1117 then begin
1118 dec(Column);
1119 if IsIdentChar[LineSrc[Column]] then begin
1120 while (Column>1) and (IsIdentChar[LineSrc[Column-1]]) do
1121 dec(Column);
1122 end;
1123 end;
1124 end;
1125 end;
1126 Result:=TETMark.Create(aSynEdit);
1127 Result.SourceMarks:=Self;
1128 Result.MsgLine:=MsgLine;
1129 Result.Line:=Line;
1130 Result.Column:=Column;
1131 Result.Visible:=true;
1132 Result.Priority:=Priority;
1133 Result.Urgency:=MsgLine.Urgency;
1134 Result.ImageList:=ImageList;
1135 Result.ImageIndex:=MarkStyles[Result.Urgency].ImageIndex;
1136 Result.SourceMarkup:=MarkStyles[Result.Urgency].SourceMarkup;
1137 aSynEdit.Marks.Add(Result);
1138 end;
1139
1140 procedure TETMarks.RemoveMarks(aSynEdit: TSynEdit);
1141 var
1142 i: Integer;
1143 Mark: TSynEditMark;
1144 begin
1145 for i:=aSynEdit.Marks.Count-1 downto 0 do begin
1146 Mark:=aSynEdit.Marks[i];
1147 if Mark is TETMark then
1148 Mark.Free;
1149 end;
1150 end;
1151
1152 { TETMarkStyle }
1153
1154 procedure TETMarkStyle.SetColor(AValue: TColor);
1155 begin
1156 if FColor=AValue then Exit;
1157 FColor:=AValue;
1158 SourceMarkup.FrameColor:=Color;
1159 end;
1160
1161 constructor TETMarkStyle.Create(TheUrgency: TMessageLineUrgency;
1162 TheColor: TColor);
1163 begin
1164 FUrgency:=TheUrgency;
1165 FColor:=TheColor;
1166 FSourceMarkup:=TSynSelectedColor.Create;
1167 SourceMarkup.Foreground:=clNone;
1168 SourceMarkup.Background:=clNone;
1169 SourceMarkup.FrameStyle:=slsWaved;
1170 SourceMarkup.FrameEdges:=sfeBottom;
1171 SourceMarkup.FrameColor:=Color;
1172 end;
1173
1174 destructor TETMarkStyle.Destroy;
1175 begin
1176 FreeAndNil(FSourceMarkup);
1177 inherited Destroy;
1178 end;
1179
1180 { TLMsgViewLine }
1181
1182 destructor TLMsgViewLine.Destroy;
1183 begin
1184 FreeAndNil(Mark);
1185 inherited Destroy;
1186 end;
1187
1188 end.
1189
1190