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 (*
22 Modified by Kevin Jesshope <KevinOfOz@gmail.com> 15 Mar 2020
23 - Owner and Category now support basic quoted values
24 -c'Lazarus ToDoList'
25 - Select Token style from dialog. #todo is normal (unchecked) todo is alernate (checked)
26 - Save Owner, Category and normal/alt selection to XMLPropStorage
27 - Move (some) non-presentation code to ToDoListCore
28 - Add Note type to ToDo and Done types
29 *)
30
31 unit ToDoListCore;
32
33 {$mode objfpc}{$H+}
34
35 interface
36
37 uses
38 // FCL, RTL
39 Classes, SysUtils, StrUtils, Laz_AVL_Tree,
40 // LCL
41 LCLType, LclIntf, Controls, Dialogs, ComCtrls,
42 // LazUtils
43 LazFileUtils, LazStringUtils, LazFileCache, LazLoggerBase,
44 // Codetools
45 CodeToolManager, FileProcs, CodeCache, BasicCodeTools,
46 // IDEIntf
47 PackageIntf, ProjectIntf,
48 // ToDoList
49 ToDoListStrConsts;
50
51 type
52 TToDoType = (tdToDo, tdDone, tdNote);
53 TTokenStyle = (tsNormal, tsAlternate);
54
55 const
56 LIST_INDICATORS : array [TToDoType] of string
57 = ('ToDo', 'Done', 'Note');
58
59 // Value names of the various parts of the todo entry
60 OWNER_PART_NAME = 'Owner';
61 CATEGORY_PART_NAME = 'Category';
62 PRIORITY_PART_NAME = 'Priority';
63 TEXT_PART_NAME = 'Text';
64
65
66 type
67 TTLScannedFile = class;
68
69 { TTodoItem: Class to hold TODO item information }
70
71 TTodoItem = class(TObject)
72 private
73 FCategory: string;
74 FToDoType:TToDoType;
75 FTokenStyle:TTokenStyle;
76 FFilename: string;
77 FLineNumber: integer;
78 FOwner: string;
79 FPriority: integer;
80 FText: string;
81 FTLFile: TTLScannedFile;
GetQuotedCategorynull82 function GetQuotedCategory: string;
GetQuotedOwnernull83 function GetQuotedOwner: string;
GetAsCommentnull84 function GetAsComment: string;
GetAsStringnull85 function GetAsString: string;
QuotedStrnull86 function QuotedStr(const aSrc: string; const aQuote: char): string;
87 public
88 constructor Create(aTLFile: TTLScannedFile);
89 property TLFile: TTLScannedFile read FTLFile;
90 property Category: string read FCategory write FCategory;
91 property QuotedCategory:string read GetQuotedCategory;
92 property TokenStyle: TTokenStyle read FTokenStyle write FTokenStyle;
93 property ToDoType:TToDoType read FToDoType write FToDoType;
94 property LineNumber: integer read FLineNumber write FLineNumber;
95 property Filename: string read FFilename write FFilename;
96 property Owner: string read FOwner write FOwner;
97 property QuotedOwner:string read GetQuotedOwner;
98 property Priority: integer read FPriority write FPriority;
99 property Text: string read FText write FText;
100 property AsString: string read GetAsString;
101 property AsComment: string read GetAsComment;
102 end;
103
104 { TTLScannedFiles }
105
106 TTLScannedFile = class
107 FItems: TFPList;// list of TTodoItem
108 private
GetCountnull109 function GetCount: integer;
GetItemsnull110 function GetItems(Index: integer): TTodoItem;
111 public
112 Filename: string; // = Tool.MainFilename
113 CodeChangeStep: integer; // = Tool.Scanner.ChangeStep
114 destructor Destroy; override;
115 procedure Clear;
116 procedure Add(aItem: TTodoItem);
117 property Count: integer read GetCount;
118 property Items[Index: integer]: TTodoItem read GetItems; default;
119 end;
120
121 { TToDoListCore }
122
123 (* implemented as a class of class procedures so the protected methods can be
124 exposed via a class helper for unit testing purposes *)
125
126 TToDoListCore = class(TObject)
127 protected
128 class procedure ParseToParts(const aTokenString:string;const aParts:TStrings);
129 class procedure AddToDoItemFromParts(const aParts: TStrings;
130 const aTLFile: TTLScannedFile; const aFileName: string;
131 const aLineNumber: integer; const aToDoType: TToDoType;
132 const aTokenStyle: TTokenStyle);
133 public
134 class procedure CreateToDoItem(aTLFile: TTLScannedFile;
135 const aFileName: string; const aStartComment, aEndComment: string;
136 const aTokenString: string; aLineNumber: Integer);
137 class procedure ExtractToCSV(const aListItems: TListItems; const aFilename: string);
138 class procedure ScanFile(const aFileName: string; const FScannedFiles: TAvlTree
139 );
140 end;
141
142
143 implementation
144
145 const
146 TODO_TOKENS : array [TTokenStyle, TToDoType] of string
147 = (('#todo', '#done', '#note'), ('TODO', 'DONE', 'NOTE'));
148
CompareAnsiStringWithTLScannedFilenull149 function CompareAnsiStringWithTLScannedFile(Filename, ScannedFile: Pointer): integer;
150 begin
151 Result:=CompareFilenames(AnsiString(Filename),
152 TTLScannedFile(ScannedFile).Filename);
153 end;
154
155 { TToDoListCore }
156
157 type
158 TParseState = (psHunting, psGotDash, psOwnerStart, psOwnerContinue, psCategoryStart,
159 psCategoryContinue, psPriority, psText, psAllDone); { NOTE : Continue state must follow Start state }
160
161 class procedure TToDoListCore.ParseToParts(const aTokenString: string;
162 const aParts: TStrings);
163
ParseStateToTextnull164 function ParseStateToText(const aParseState:TParseState):string;
165 begin
166 case aParseState of
167 psOwnerStart, psOwnerContinue:Result := OWNER_PART_NAME;
168 psCategoryStart,psCategoryContinue:Result := CATEGORY_PART_NAME;
169 psPriority:Result := PRIORITY_PART_NAME;
170 psText:Result := TEXT_PART_NAME;
171 else
172 raise Exception.Create(excInvalidParseState);
173 end;
174 end;
175
176 var
177 lParseState:TParseState;
178 i, lPriorityStart, lBytesRemoved: Integer;
179 lTempStr, lStr: string;
180 lpTemp: PChar;
181 begin
182 lParseState :=psHunting;
183 i := 1;
184 while i <= Length(aTokenString) do
185 begin
186 case lParseState of
187
188 psHunting:
189 begin
190 case aTokenString[i] of
191 ' ':Inc(i);// look at the next character
192 '-':
193 begin
194 lParseState:=psGotDash;
195 Inc(i);
196 end;
197 '0'..'9':
198 begin
199 lParseState:=psPriority;
200 lPriorityStart := i;
201 Inc(i);
202 end;
203 ':':
204 begin
205 lParseState:=psText;
206 Inc(i);
207 end;
208 else // Not a special character so it must be the text
209 lParseState := psText;
210 end;
211 end;
212
213 psText:
214 begin
215 aParts.Values[ParseStateToText(lParseState)]:= Trim(Copy(aTokenString, i, MaxInt));
216 lParseState := psAllDone;
217 end;
218
219 psGotDash:
220 begin
221 case LowerCase(aTokenString[i]) of
222 'o':
223 begin
224 lParseState:=psOwnerStart;
225 Inc(i);
226 end;
227 'c':
228 begin
229 lParseState:=psCategoryStart;
230 Inc(i);
231 end
232 else // invalid so assume rest is text
233 begin
234 lParseState := psText;
235 Dec(i); // wind back 1 character so we catch the - in the text
236 end;
237 end;
238 end;
239
240 psPriority:
241 if (aTokenString[i] < '0') or (aTokenString[i] > '9') then
242 begin
243 aParts.Values[ParseStateToText(lParseState)] := Copy(aTokenString, lPriorityStart, i-lPriorityStart);
244 lParseState := psHunting;
245 end
246 else
247 Inc(i);
248
249 psOwnerStart, psCategoryStart:
250 begin
251 case aTokenString[i] of
252 '''':// Got a quote so extract
253 begin
254 lTempStr := Copy(aTokenString, i, MaxInt);
255 lpTemp := PChar(lTempStr);
256 lStr := AnsiExtractQuotedStr(lpTemp, '''');
257 aParts.Values[ParseStateToText(lParseState)] := lStr;
258 lBytesRemoved := Length(lTempStr) - Length(lpTemp);
259 i := i + lBytesRemoved;
260 lParseState := psHunting;
261 end;
262 else
263 begin
264 lTempStr := aTokenString[i];
265 Inc(i);
266 Assert(Succ(psOwnerStart) = psOwnerContinue, 'Succ(psOwnerStart) is not psOwnerContinue.');
267 Assert(Succ(psCategoryStart) = psCategoryContinue, 'Succ(psCategoryStart) is not psCategoryContinue.');
268 inc(lParseState); // Assumes Continue is succ to Start
269 end;
270 end;
271 end;
272
273 psOwnerContinue,psCategoryContinue:
274 begin
275 if (aTokenString[i] = ' ') or (aTokenString[i] = ':') then
276 begin
277 aParts.Values[ParseStateToText(lParseState)] := lTempStr;
278 lParseState:=psHunting;
279 end
280 else
281 begin
282 lTempStr:=lTempStr + aTokenString[i];
283 Inc(i);
284 end;
285 end;
286
287 psAllDone:
288 break;
289
290 end;
291 end;
292 end;
293
294 class procedure TToDoListCore.AddToDoItemFromParts(const aParts: TStrings;
295 const aTLFile: TTLScannedFile; const aFileName: string;
296 const aLineNumber: integer; const aToDoType: TToDoType;
297 const aTokenStyle: TTokenStyle);
298
299 var
300 lNewToDoItem: TTodoItem;
301
302 begin
303 lNewToDoItem := TTodoItem.Create(aTLFile);
304 lNewToDoItem.ToDoType := aToDoType;
305 lNewToDoItem.TokenStyle := aTokenStyle;
306 lNewToDoItem.LineNumber := aLineNumber;
307 lNewToDoItem.Filename := aFileName;
308
309 if aParts.Values[TEXT_PART_NAME] <> '' then
310 lNewToDoItem.Text:=aParts.Values[TEXT_PART_NAME];
311
312 if aParts.Values[OWNER_PART_NAME] <> '' then
313 lNewToDoItem.Owner:=aParts.Values[OWNER_PART_NAME];
314
315 if aParts.Values[CATEGORY_PART_NAME] <> '' then
316 lNewToDoItem.Category:=aParts.Values[CATEGORY_PART_NAME];
317
318 if aParts.Values[PRIORITY_PART_NAME] <> '' then
319 lNewToDoItem.Priority:=StrToInt(aParts.Values[PRIORITY_PART_NAME]);
320
321 if Assigned(aTLFile) then
322 aTLFile.Add(lNewToDoItem);
323 end;
324
325 class procedure TToDoListCore.CreateToDoItem(aTLFile: TTLScannedFile;
326 const aFileName: string; const aStartComment, aEndComment: string;
327 const aTokenString: string; aLineNumber: Integer);
328
329 var
330 lParsingString, lTokenToCheckFor : string;
331 lToDoTokenFound: boolean;
332 lTodoType, lFoundToDoType: TToDoType;
333 lTokenStyle, lFoundTokenStyle: TTokenStyle;
334 lParts: TStringList;
335
336 begin
337 //DebugLn(['TfrmTodo.CreateToDoItem aFileName=',aFileName,' LineNumber=',aLineNumber]);
338 lParsingString:= TextToSingleLine(aTokenString);
339 // Remove the beginning comment chars from input string
340 Delete(lParsingString, 1, Length(aStartComment));
341 // Remove leading and trailing blanks from input
342 lParsingString := Trim(lParsingString);
343 // See if it's a TODO or DONE item
344
345 lToDoTokenFound:=False;
346
347 // Determine token and style
348
349 for lTokenStyle := Low(TTokenStyle) to High(TTokenStyle) do
350 begin
351 for lTodoType := Low(TToDoType) to High (TToDoType) do
352 begin
353 lTokenToCheckFor := TODO_TOKENS[lTokenStyle, lTodoType];
354 if (LazStartsText(lTokenToCheckFor, lParsingString)) // Token match
355 and ( (Length(lParsingString)=Length(lTokenToCheckFor)) // Exact match, no further chars. Should not happen?
356 or not (lParsingString[Length(lTokenToCheckFor)+1] in ['A'..'Z','a'..'z'])
357 ) then // Extra char is not alpha
358 begin
359 lToDoTokenFound := True;
360 lFoundToDoType := lTodoType;
361 lFoundTokenStyle := lTokenStyle;
362 Break;
363 end;
364 if lToDoTokenFound then
365 break;
366 end;
367 end;
368
369 if Not lToDoTokenFound then
370 Exit; // Not a Todo/Done item, leave
371
372 // Remove the ending comment chars from input string
373 if (aEndComment <> '') and LazEndsStr(aEndComment, lParsingString) then
374 SetLength(lParsingString, Length(lParsingString)-Length(aEndComment));
375
376 // Remove the Token
377 Delete(lParsingString, 1, Length(TODO_TOKENS[lFoundTokenStyle, lTodoType]));
378
379 lParsingString := Trim(lParsingString);
380
381 lParts:=TStringList.Create;
382 try
383 ParseToParts(lParsingString, lParts);
384 AddToDoItemFromParts(lParts, aTLFile, aFileName, aLineNumber, lFoundToDoType, lFoundTokenStyle);
385 finally
386 lParts.Free;
387 end;
388
389 end;
390
391 class procedure TToDoListCore.ExtractToCSV(const aListItems:TListItems;const aFilename:string);
392 var
393 lCommaList: TStringList;
394 i: Integer;
395 lToDoItem: TTodoItem;
396 s, t: String;
397 begin
398 lCommaList:=TStringList.Create;
399 try
400 lCommaList.Add(csvHeader);
401 i:=0;
402 while i<aListItems.Count do
403 begin
404 lToDoItem:=TTodoItem(aListItems[i].Data);
405 s:=LIST_INDICATORS[lToDoItem.ToDoType] + ',';
406 t:=DelChars(lToDoItem.Text,',');{Strip any commas that can cause a faulty csv file}
407 s:=s+t+','+IntToStr(lToDoItem.Priority)+','+lToDoItem.Filename+
408 ','+IntToStr(lToDoItem.LineNumber)+','+lToDoItem.Owner+','+lToDoItem.Category;
409 lCommaList.Add(s);
410 Inc(i);
411 end;
412 lCommaList.SaveToFile(aFileName);
413 finally
414 lCommaList.Clear;
415 lCommaList.Free;
416 end;
417 end;
418
419 class procedure TToDoListCore.ScanFile(const aFileName: string;const FScannedFiles:TAvlTree);
420 var
421 ExpandedFilename: String;
422 AVLNode: TAvlTreeNode;
423 Tool: TCodeTool;
424 Code: TCodeBuffer;
425 CurFile: TTLScannedFile;
426 Src: String;
427 p: Integer;
428 NestedComment: Boolean;
429 CommentEnd: LongInt;
430 CommentStr: String;
431 CodeXYPosition: TCodeXYPosition;
432 begin
433 //DebugLn(['TfrmTodo.ScanFile ',aFileName]);
434 ExpandedFilename:=TrimFilename(aFileName);
435
436 Code:=CodeToolBoss.LoadFile(ExpandedFilename,true,false);
437
438 if Code=nil then begin
439 debugln(['TIDETodoWindow.ScanFile failed loading ',ExpandedFilename]);
440 exit;
441 end;
442
443 CodeToolBoss.Explore(Code,Tool,false,false); // ignore the result
444
445 if (Tool=nil) or (Tool.Scanner=nil) then begin
446 debugln(['TIDETodoWindow.ScanFile failed parsing ',Code.Filename]);
447 exit;
448 end;
449
450 AVLNode:=FScannedFiles.FindKey(Pointer(Tool.MainFilename),
451 @CompareAnsiStringWithTLScannedFile);
452 CurFile:=nil;
453 //DebugLn(['TfrmTodo.ScanFile ',Tool.MainFilename,' AVLNode=',AVLNode<>nil]);
454 if AVLNode<>nil then begin
455 CurFile:=TTLScannedFile(AVLNode.Data);
456 // Abort if this file has already been scanned and has not changed
457 if CurFile.CodeChangeStep=Tool.Scanner.ChangeStep then exit;
458 end;
459 //DebugLn(['TfrmTodo.ScanFile SCANNING ... ']);
460
461 // Add file name to list of scanned files
462 if CurFile=nil then begin
463 CurFile:=TTLScannedFile.Create;
464 CurFile.Filename:=Tool.MainFilename;
465 FScannedFiles.Add(CurFile);
466 end;
467 // save ChangeStep
468 CurFile.CodeChangeStep:=Tool.Scanner.ChangeStep;
469 //DebugLn(['TfrmTodo.ScanFile saved ChangeStep ',CurFile.CodeChangeStep,' ',Tool.Scanner.ChangeStep]);
470 // clear old items
471 CurFile.Clear;
472 Src:=Tool.Src;
473 p:=1;
474 NestedComment:=CodeToolBoss.GetNestedCommentsFlagForFile(Code.Filename);
475
476 repeat
477 p:=FindNextComment(Src,p);
478 if p>length(Src) then break;
479 CommentEnd:=FindCommentEnd(Src,p,NestedComment);
480 if not Tool.CleanPosToCaret(p,CodeXYPosition) then
481 begin
482 ShowMessageFmt(errScanFileFailed, [ExtractFileName(aFileName)]);
483 Exit;
484 end;
485
486 CommentStr:=copy(Src,p,CommentEnd-p);
487 //DebugLn(['TfrmTodo.ScanFile CommentStr="',CommentStr,'"']);
488 if Src[p]='/' then
489 CreateToDoItem(CurFile,CodeXYPosition.Code.Filename, '//', '', CommentStr, CodeXYPosition.Y)
490 else if Src[p]='{' then
491 CreateToDoItem(CurFile,CodeXYPosition.Code.Filename, '{', '}', CommentStr, CodeXYPosition.Y)
492 else if Src[p]='(' then
493 CreateToDoItem(CurFile,CodeXYPosition.Code.Filename, '(*', '*)', CommentStr, CodeXYPosition.Y);
494 p:=CommentEnd;
495 until false;
496
497 end;
498
499 { TTLScannedFile }
500
GetCountnull501 function TTLScannedFile.GetCount: integer;
502 begin
503 if Assigned(FItems) then
504 Result:=FItems.Count
505 else
506 Result:=0
507 end;
508
GetItemsnull509 function TTLScannedFile.GetItems(Index: integer): TTodoItem;
510 begin
511 Result:=TTodoItem(FItems[Index]);
512 end;
513
514 destructor TTLScannedFile.Destroy;
515 begin
516 Clear;
517 inherited Destroy;
518 end;
519
520 procedure TTLScannedFile.Clear;
521 var
522 i: Integer;
523 begin
524 if Assigned(FItems) then
525 begin
526 for i:=0 to FItems.Count-1 do
527 TObject(FItems[i]).Free;
528 FreeAndNil(FItems);
529 end;
530 end;
531
532 procedure TTLScannedFile.Add(aItem: TTodoItem);
533 begin
534 if not Assigned(FItems) then
535 FItems:=TFPList.Create;
536
537 FItems.Add(aItem);
538 end;
539
540 { TTodoItem }
541
GetAsStringnull542 function TTodoItem.GetAsString: string;
543 begin
544 Result := TODO_TOKENS[TokenStyle, ToDoType];
545 // Priority
546 if Priority > 0 then
547 Result := Result + ' '+IntToStr(Priority);
548 // Owner
549 if Owner <> '' then
550 Result := Result + ' -o'+QuotedOwner;
551 // Category
552 if Category <> '' then
553 Result := Result + ' -c'+QuotedCategory;
554 // Text
555 Result := Result + ' : ' + Text;
556 end;
557
QuotedStrnull558 function TTodoItem.QuotedStr(const aSrc: string; const aQuote: char): string;
559 begin
560 // Only quote if necessary
561 if (pos(aQuote, aSrc)<>0)
562 or (pos(' ', aSrc)<>0) then
563 Result := AnsiQuotedStr(aSrc, aQuote)
564 else
565 Result := aSrc;
566 end;
567
568 constructor TTodoItem.Create(aTLFile: TTLScannedFile);
569 begin
570 FTLFile:=aTLFile;
571 end;
572
GetQuotedOwnernull573 function TTodoItem.GetQuotedOwner: string;
574 begin
575 Result := QuotedStr(FOwner, '''');
576 end;
577
GetQuotedCategorynull578 function TTodoItem.GetQuotedCategory: string;
579 begin
580 Result := QuotedStr(FCategory, '''');
581 end;
582
GetAsCommentnull583 function TTodoItem.GetAsComment: string;
584 begin
585 Result := '{ '+AsString+' }';
586 end;
587
588 end.
589