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     Functions to substitute code macros.
25     Dialog to setup interactive macros by the programmer.
26 }
27 unit CodeMacroPrompt;
28 
29 {$mode objfpc}{$H+}
30 
31 interface
32 
33 uses
34   Classes, SysUtils, LCLProc, Forms, Controls, Graphics, Dialogs,
35   BasicCodeTools, CodeToolManager,
36   SynEditAutoComplete, SynPluginTemplateEdit, SynPluginSyncronizedEditBase, SynEdit, SynEditTypes,
37   LazIDEIntf, SrcEditorIntf, LazUTF8;
38 
39 type
40   TCodeMacroPromptDlg = class(TForm)
41   private
42     { private declarations }
43   public
44     { public declarations }
45   end;
46 
47   TLazSynPluginSyncronizedEditCell = class(TSynPluginSyncronizedEditCell)
48   public
49     CellValue: String;
50   end;
51 
52   { TLazSynPluginSyncronizedEditList }
53 
54   TLazSynPluginSyncronizedEditList = class(TSynPluginSyncronizedEditList)
55   private
56   public
AddNewnull57     function AddNew: TSynPluginSyncronizedEditCell; override;
58   end;
59 
60   { TLazTemplateParser }
61 
62   TLazTemplateParser = class(TIDETemplateParser)
63   private
64     FCaret: TPoint;
65     FEditCellList: TSynPluginSyncronizedEditList;
66     FEnableMacros: Boolean;
67     FIndent: integer;
68     FKeepSubIndent: Boolean;
69     FSrcTemplate: String;
70     FDestTemplate: String;
71     FSrcPosition: Integer;
72     FDestPosition: Integer;
73     FDestPosX: Integer;
74     FDestPosY: Integer;
75     FLevel: Integer;
76     FSrcEdit: TSourceEditorInterface;
77     FSubIndent: integer;
78     FUseTabWidth: integer;
79   protected
80     // nested macros, get the X pos of the outer macro
GetSrcPositionnull81     function GetSrcPosition: Integer; override;
GetDestPositionnull82     function GetDestPosition: Integer; override;
GetDestPosXnull83     function GetDestPosX: Integer; override;
GetDestPosYnull84     function GetDestPosY: Integer; override;
GetSrcTemplatenull85     function GetSrcTemplate: String; override;
GetDestTemplatenull86     function GetDestTemplate: String; override;
87 
SubstituteMacronull88     function SubstituteMacro(const MacroName, MacroParameter: string;
89                              out MacroValue: string): boolean;
SubstituteMacrosnull90     function SubstituteMacros(var Template: String): boolean;
91   public
92     constructor Create(TheTemplate: String);
93     destructor Destroy; override;
94 
SubstituteCodeMacrosnull95     function SubstituteCodeMacros(SrcEdit: TSourceEditorInterface): boolean;
96     procedure TrimEOTChar(eot: Char);
97 
98     property EnableMacros: Boolean read FEnableMacros write FEnableMacros;
99     property KeepSubIndent: Boolean read FKeepSubIndent write FKeepSubIndent;
100     property Indent: integer read FIndent write FIndent;
101     property SubIndent: integer read FSubIndent write FSubIndent;
102     property DestCaret: TPoint read FCaret;
103     property UseTabWidth: integer read FUseTabWidth write FUseTabWidth; // if >0 use tabs for indenting with this size
104 
105     property EditCellList: TSynPluginSyncronizedEditList read FEditCellList;
106   end;
107 
ExecuteCodeTemplatenull108 function ExecuteCodeTemplate(SrcEdit: TSourceEditorInterface;
109   const TemplateName, TemplateValue, {%H-}TemplateComment,
110   EndOfTokenChr: string; Attributes: TStrings;
111   IndentToTokenStart: boolean): boolean;
112 
113 implementation
114 
115 {$R *.lfm}
116 
117 const
118   MaxLevel = 10; // prevent cycling
119 
120 { TLazTemplateParser }
121 
122 constructor TLazTemplateParser.Create(TheTemplate: String);
123 begin
124   inherited Create;
125   FEditCellList := TLazSynPluginSyncronizedEditList.Create;
126   FSrcTemplate := TheTemplate;
127   FDestTemplate := '';
128   FSrcPosition := 1;
129   FDestPosition := 1;
130   FDestPosX := 1;
131   FDestPosY := 1;
132   FLevel := 0;
133   FCaret.y := -1;
134 end;
135 
136 destructor TLazTemplateParser.Destroy;
137 begin
138   FEditCellList.Free;
139   inherited Destroy;
140 end;
141 
GetSrcPositionnull142 function TLazTemplateParser.GetSrcPosition: Integer;
143 begin
144   Result := FSrcPosition;
145 end;
146 
GetDestPositionnull147 function TLazTemplateParser.GetDestPosition: Integer;
148 begin
149   Result := FDestPosition;
150 end;
151 
GetDestPosXnull152 function TLazTemplateParser.GetDestPosX: Integer;
153 begin
154   Result := FDestPosX;
155 end;
156 
TLazTemplateParser.GetDestPosYnull157 function TLazTemplateParser.GetDestPosY: Integer;
158 begin
159   Result := FDestPosY;
160 end;
161 
TLazTemplateParser.GetSrcTemplatenull162 function TLazTemplateParser.GetSrcTemplate: String;
163 begin
164   Result := FSrcTemplate;
165 end;
166 
TLazTemplateParser.GetDestTemplatenull167 function TLazTemplateParser.GetDestTemplate: String;
168 begin
169   Result := FDestTemplate;
170 end;
171 
TLazTemplateParser.SubstituteMacronull172 function TLazTemplateParser.SubstituteMacro(const MacroName,
173   MacroParameter: string; out MacroValue: string): boolean;
174 var
175   Macro: TIDECodeMacro;
176   NewValue: String;
177   ErrMsg: string;
178 begin
179   Result := false;
180   MacroValue:='';
181   Macro := IDECodeMacros.FindByName(MacroName);
182   //debugln('SubstituteMacro A ',MacroName,' ',dbgs(Macro<>nil),' ',MacroParameter);
183   if Macro <> nil then begin
184     // substitute macros in Parameter
185     MacroValue := MacroParameter;
186     if (FLevel < MaxLevel) and (not SubstituteMacros(MacroValue)) then
187       exit;
188 
189     if Macro.Interactive then begin
190       // collect interactive macro
191       debugln('SubstitutMacros TODO interactive macros');
192     end else begin
193       // normal macro -> substitute
194       NewValue:='';
195       try
196         if not Macro.GetValue(MacroValue, nil, FSrcEdit, NewValue, ErrMsg, self)
197         then
198           exit;
199       except
200         exit;
201       end;
202       MacroValue:=NewValue;
203     end;
204   end else begin
205     // macro unknown
206     MacroValue:='UnknownMacro('+MacroName+')';
207   end;
208   if ErrMsg='' then ;
209   Result:=true;
210 end;
211 
SubstituteMacrosnull212 function TLazTemplateParser.SubstituteMacros(var Template: String): boolean;
213 const
214   TemplateTabWidth = 8;
215 var
216   IndentLevel: Integer;
217   CurLineIndent, LastLineIndent: Integer;
218   IsLineStart: boolean;
219 
220   procedure AddIndent;
221   var
222     i: Integer;
223     s: String;
224   begin
225     // compare the indentation of the current and the last line of the template
226     if CurLineIndent>LastLineIndent then
227       inc(IndentLevel)
228     else if (IndentLevel>0) and (CurLineIndent<LastLineIndent) then
229       dec(IndentLevel);
230     //debugln(['AddIndent LastLineIndent=',LastLineIndent,' CurLineIndent=',CurLineIndent]);
231     LastLineIndent:=CurLineIndent;
232     // append space
233     i:=Indent+IndentLevel*SubIndent;
234     //debugln(['AddIndent Indent=',Indent,' IndentLevel=',IndentLevel,' SubIndent=',SubIndent,' UseTabWidth=',UseTabWidth]);
235     s:=GetIndentStr(i,UseTabWidth);
236     FDestTemplate += s;
237     FDestPosX:=length(s)+1;
238   end;
239 
240   procedure AppendToDest(S: String);
241   // only called when FLevel=1
242   var
243     p, LastCopy: Integer;
244   begin
245     if IsLineStart then begin
246       // remove old indent
247       p:=length(FDestTemplate);
248       while (p>=1) and (FDestTemplate[p] in [' ',#9]) do dec(p);
249       FDestTemplate:=LeftStr(FDestTemplate,p);
250     end;
251     p := 1;
252     LastCopy := 1;
253     //debugln(['AppendToDest START S="',dbgstr(S),'" Indent=',Indent,' IsLineStart=',IsLineStart,' KeepSubIndent=',KeepSubIndent]);
254     while p <= length(S) do begin
255       if IsLineStart and (not KeepSubIndent) and (FDestTemplate<>'')
256       then begin
257         // at start of template line (not first line)
258         LastCopy:=p;
259         case s[p] of
260         ' ':
261           begin
262             inc(CurLineIndent);
263             inc(p);
264             continue;
265           end;
266         #9:
267           begin
268             inc(p);
269             inc(CurLineIndent,TemplateTabWidth);
270             CurLineIndent:=CurLineIndent-(CurLineIndent mod TemplateTabWidth);
271             continue;
272           end;
273         else
274           // first character of line
275           IsLineStart:=false;
276           LastCopy:=p;
277           AddIndent;
278           // keep p and handle the character in the next step
279         end;
280       end;
281 
282       case s[p] of
283       #10, #13:
284         begin
285           // copy line break
286           inc(p);
287           if (p <= length(S)) and (s[p] in [#10,#13]) and (s[p] <> s[p-1]) then
288             inc(p);
289           //debugln(['AppendToDest linebreak flush "',dbgstr(copy(s, LastCopy, p - LastCopy)),'"']);
290           FDestTemplate += copy(s, LastCopy, p - LastCopy);
291           LastCopy := p;
292           FDestPosX := 1 + Indent;
293           IsLineStart:=true;
294           CurLineIndent:=0;
295           inc(FDestPosY);
296         end;
297       else // case else
298         if (s[p] = '|') and (FCaret.y < 0) then
299         begin
300           // place cursor
301           System.Delete(s, p, 1);
302           FCaret.y := FDestPosY;
303           FCaret.x := FDestPosX;
304           //debugln(['AppendToDest Caret=',dbgs(FCaret)]);
305         end
306         else begin
307           inc(p);
308           inc(FDestPosX);
309           IsLineStart:=false;
310         end;
311       end;
312     end;
313     //debugln(['AppendToDest LAST flush "',dbgstr(copy(s, LastCopy, p - LastCopy)),'"']);
314     if IsLineStart then
315       AddIndent
316     else
317       FDestTemplate += copy(s, LastCopy, p - LastCopy);
318     FDestPosition := length(FDestTemplate);
319     //debugln(['AppendToDest END FDestTemplate="',dbgstr(FDestTemplate),'" FDestPosition=',FDestPosition,' FDestPosX=',FDestPosX,' FDestPosY=',FDestPosY]);
320   end;
321 
322 var
323   p: LongInt;
324   len: Integer;
325   SrcCopiedPos: LongInt;
326   MacroStartPos: LongInt;
327   MacroParamStartPos: LongInt;
328   MacroParamEndPos: LongInt;
329   MacroEndPos: LongInt;
330   MacroName: String;
331   MacroParameter: String;
332   MacroValue: string;
333   Lvl: Integer;
334   SaveSrcPos: LongInt;
335 begin
336   // replace as many macros as possible
337   inc(FLevel);
338   p:=1;
339   SrcCopiedPos := 1;
340   len:=length(Template);
341   IndentLevel:=0;
342   CurLineIndent:=0;
343   LastLineIndent:=0;
344   IsLineStart:=false;
345   while p <= len do begin
346     case Template[p] of
347       '$':
348         begin
349           inc(p);
350           // could be a macro start
351           MacroStartPos := p - 1;
352           MacroEndPos   := -1;
353 
354           if (p <= len) and (Template[p]='$') then begin
355             System.Delete(Template, p, 1);
356             inc(FSrcPosition);
357           end
358           else begin
359             // find the macro end
360             while (p <= len) and (Template[p] in ['a'..'z','A'..'Z','0'..'9','_']) do
361               inc(p);
362             if FEnableMacros and (p <= len) and (p-MacroStartPos > 1) and
363                (Template[p] = '(') then
364             begin
365               inc(p);
366               MacroParamStartPos:=p;
367               Lvl:=1;
368               while (p<=len) and (Lvl>0) do begin
369                 case Template[p] of
370                 '(': inc(Lvl);
371                 ')': dec(Lvl);
372                 end;
373                 inc(p);
374               end;
375               if Lvl=0 then begin
376                 // macro parameter end found
377                 MacroParamEndPos:=p-1;
378                 MacroEndPos:=p;
379               end;
380             end;
381           end;
382 
383           if MacroEndPos < 0 then begin
384             // not a macro
385             p := MacroStartPos + 1;
386             inc(FSrcPosition);
387           end
388           else begin
389             // Got a Macro
390             if FLevel = 1 then begin
391               AppendToDest(copy(Template, SrcCopiedPos, MacroStartPos - SrcCopiedPos));
392             end;
393             FSrcPosition := FSrcPosition + MacroEndPos - MacroStartPos;
394             // read macro name
395             MacroName:=copy(Template,MacroStartPos+1,
396                                           MacroParamStartPos-MacroStartPos-2);
397             MacroParameter:=copy(Template,MacroParamStartPos,
398                                          MacroParamEndPos-MacroParamStartPos);
399 
400             SaveSrcPos := FSrcPosition;
401             if not SubstituteMacro(MacroName,MacroParameter,MacroValue) then
402               exit(false);
403             FSrcPosition := SaveSrcPos;
404             //debugln('SubstituteMacros MacroName="',MacroName,'" MacroParameter="',MacroParameter,'" MacroValue="',MacroValue,'"');
405 
406             Template := copy(Template, 1, MacroStartPos-1)
407                       + MacroValue
408                       + copy(Template, MacroEndPos, len);
409             len:=length(Template);
410             p:=MacroStartPos+length(MacroValue);
411             SrcCopiedPos := p;
412             // scan the result for new lines
413             if FLevel = 1 then
414               AppendToDest(MacroValue);
415           end;
416           // else it is a normal $ character
417         end;
418       else
419         begin
420           inc(p);
421           inc(FSrcPosition);
422         end;
423     end;
424   end;
425 
426   if FLevel = 1 then begin
427     AppendToDest(copy(Template, SrcCopiedPos, p - SrcCopiedPos));
428     if IsLineStart and (not KeepSubIndent) and (FDestTemplate<>'') then
429       AddIndent;
430   end;
431   dec(FLevel);
432   Result:=true;
433 end;
434 
TLazTemplateParser.SubstituteCodeMacrosnull435 function TLazTemplateParser.SubstituteCodeMacros(SrcEdit: TSourceEditorInterface): boolean;
436 begin
437   FDestTemplate := '';
438   FDestPosition := 1;
439   FDestPosX := 1;
440   FDestPosY := 1;
441   FLevel := 0;
442   FSrcEdit := SrcEdit;
443   Result := SubstituteMacros(FSrcTemplate);
444 end;
445 
446 procedure TLazTemplateParser.TrimEOTChar(eot: Char);
447 begin
448   if (FDestTemplate <> '') and (FDestTemplate[length(FDestTemplate)] = eot) then
449     System.Delete(FDestTemplate, length(FDestTemplate), 1);
450 end;
451 
ExecuteCodeTemplatenull452 function ExecuteCodeTemplate(SrcEdit: TSourceEditorInterface;
453   const TemplateName, TemplateValue, TemplateComment,
454   EndOfTokenChr: string; Attributes: TStrings;
455   IndentToTokenStart: boolean): boolean;
456 var
457   AEditor: TSynEdit;
458   p: TPoint;
459   TokenStartX: LongInt;
460   s: string;
461   BaseIndent, LogBaseIndent: Integer;
462   i: Integer;
463   j: LongInt;
464   Pattern: String;
465   LineText: String;
466   Parser: TLazTemplateParser;
467   CodeToolBossOriginalIndent : Integer; // So I don't break anything else (hopefully)
468 begin
469   Result:=false;
470   //debugln('ExecuteCodeTemplate ',dbgsName(SrcEdit),' ',dbgsName(SrcEdit.EditorControl));
471   AEditor:=SrcEdit.EditorControl as TSynEdit;
472   Pattern:=TemplateValue;
473   Parser := TLazTemplateParser.Create(Pattern);
474   AEditor.BeginUpdate;
475   try
476     Parser.SubIndent:=AEditor.BlockIndent+AEditor.BlockTabIndent*AEditor.TabWidth;
477     if (AEditor.BlockTabIndent=0) or (eoTabsToSpaces in AEditor.Options) then
478       Parser.UseTabWidth:=0
479     else
480       Parser.UseTabWidth:=AEditor.BlockTabIndent*AEditor.TabWidth;
481 
482     p := AEditor.LogicalCaretXY;
483     TokenStartX:=p.x;
484     if IndentToTokenStart then begin
485       BaseIndent := TokenStartX - 1;
486     end else begin
487       // indent the same as the first line
488       BaseIndent:=1;
489       if (p.y>0) and (p.y<=AEditor.Lines.Count) then begin
490         s:=AEditor.Lines[p.y-1];
491         while (BaseIndent<p.x)
492         and ((BaseIndent>length(s)) or (s[BaseIndent] in [#9,' '])) do
493           inc(BaseIndent);
494       end;
495       LogBaseIndent := BaseIndent - 1;
496       BaseIndent:=AEditor.LogicalToPhysicalCol(s, p.y - 1, BaseIndent);// consider tabs
497       dec(BaseIndent);
498     end;
499 
500     Parser.EnableMacros := Attributes.IndexOfName(CodeTemplateEnableMacros)>=0;
501     Parser.KeepSubIndent := Attributes.IndexOfName(CodeTemplateKeepSubIndent)>=0;
502     Parser.Indent := LogBaseIndent;
503     CodeToolBossOriginalIndent := CodeToolBoss.IndentSize;
504     if Parser.KeepSubIndent then
505       CodeToolBoss.IndentSize := BaseIndent // Use additional indentation
506     else
507       CodeToolBoss.IndentSize := 0; // Use current indentation
508     try
509       LazarusIDE.SaveSourceEditorChangesToCodeCache(nil);
510       if not Parser.SubstituteCodeMacros(SrcEdit) then exit;
511     finally
512       CodeToolBoss.IndentSize := CodeToolBossOriginalIndent;
513     end;
514 
515     s:=AEditor.Lines[p.y-1];
516     if TokenStartX>length(s) then
517       TokenStartX:=length(s)+1;
518     j:=length(TemplateName);
519     while (j>0)
520     and (UTF8CompareLatinTextFast(copy(TemplateName,1,j),copy(s,TokenStartX-j,j))<>0) do
521       dec(j);
522     dec(TokenStartX,j);
523     AEditor.BlockBegin := Point(TokenStartX, p.y);
524     AEditor.BlockEnd := p;
525 
526     // New Caret
527     p := Parser.DestCaret; // Logical
528     if p.y >= 0 then begin
529       if p.y = 1 then
530         p.x := p.x + TokenStartX - 1;
531       p.y := p.y + AEditor.BlockBegin.y - 1;
532       // p must be logical, until template text is inserted
533     end;
534 
535     // delete double end separator (e.g. avoid creating two semicolons 'begin end;;')
536     if (AEditor.BlockEnd.Y>0) and (AEditor.BlockEnd.Y<=AEditor.Lines.Count)
537     then begin
538       LineText:=AEditor.Lines[AEditor.BlockEnd.Y-1];
539       if AEditor.BlockEnd.X <= length(LineText) then
540         i := pos(LineText[AEditor.BlockEnd.X], EndOfTokenChr)
541       else
542         i := -1;
543       if i > 0 then
544         Parser.TrimEOTChar(EndOfTokenChr[i]);
545     end;
546 
547     if Parser.EditCellList.Count > 0 then
548       i := AEditor.PluginCount - 1
549     else
550       i := -1;
551     while i >= 0 do begin
552       if AEditor.Plugin[i] is TSynPluginTemplateEdit then begin
553         if p.y < 1 then
554           p := AEditor.CaretXY;
555         TSynPluginTemplateEdit(AEditor.Plugin[i]).CellParserEnabled := False;
556         TSynPluginTemplateEdit(AEditor.Plugin[i]).SetTemplate(Parser.DestTemplate, p);
557         TSynPluginTemplateEdit(AEditor.Plugin[i]).AddEditCells(Parser.EditCellList);
558         break;
559       end;
560       dec(i);
561     end;
562     if i < 0 then begin
563       // replace the selected text and position the caret
564       AEditor.SetTextBetweenPoints(AEditor.BlockBegin, AEditor.BlockEnd, Parser.DestTemplate, [], scamEnd);
565       if p.y > 0 then
566         AEditor.MoveCaretIgnoreEOL(AEditor.LogicalToPhysicalPos(p));
567     end;
568   finally
569     AEditor.EndUpdate;
570     Parser.Free;
571   end;
572   Result:=true;
573 end;
574 
575 { TLazSynPluginSyncronizedEditList }
576 
AddNewnull577 function TLazSynPluginSyncronizedEditList.AddNew: TSynPluginSyncronizedEditCell;
578 begin
579   Result := TLazSynPluginSyncronizedEditCell.Create;
580   Add(Result);
581 end;
582 
583 end.
584 
585