Subversion Repositories fastphp

Rev

Rev 98 | Rev 101 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. unit EditorMain;
  2.  
  3. {$Include 'FastPHP.inc'}
  4.  
  5. (*
  6.   This program requires
  7.   - Microsoft Internet Controls (TWebBrowser)
  8.     If you are using Delphi 10.1 Starter Edition, please import the ActiveX TLB
  9.     "Microsoft Internet Controls"
  10.   - SynEdit
  11.     You can obtain SynEdit via Embarcadero GetIt
  12. *)
  13.  
  14. // TODO: "crHourGlass" does not work if F9 is pressed!
  15. // TODO: if a scrapfile is already open, create a new scrap file (scrap2.php)
  16. // TODO: localize
  17. // TODO: wieso geht copy paste im twebbrowser nicht???
  18. // TODO: Wieso dauert webbrowser1 erste kompilierung so lange???
  19. // TODO: wieso kommt syntax fehler zweimal? einmal stderr einmal stdout?
  20. // TODO: Browser titlebar (link preview)
  21. // TODO: "jump to next/prev todo" buttons/shortcuts
  22. // TODO: "increase/decrease indent" buttons/shortcuts
  23. // TODO: Solve all compiler warnings, especially in regards Encoding
  24.  
  25. // Note in re Unicode:
  26. // - In Embarcadero® Delphi 10.4 Version 27.0.40680.4203:
  27. //   SynEdit correctly detects UTF-8 files without BOM as well as ANSI files with Umlauts.
  28. //   (Previous versions could not detect UTF-8 files without BOM?!)
  29. // - If BOM is existing, it will be removed. (which is good, because this is defined by PSR-1)
  30.  
  31. // Small things:
  32. // - The scroll bars of SynEdit are not affected by the dark theme
  33. // - dark theme full screen: doubleclick to desktop pixel "0,0" cannot be used to close the app
  34.  
  35. // Future ideas
  36. // - code insight
  37. // - verschiedene php versionen?
  38. // - webbrowser1 nur laden, wenn man den tab anw�hlt?
  39. // - doppelklick auf tab soll diesen schlie�en
  40. // - Onlinehelp (www) aufrufen oder CHM datei
  41. // - Let all colors be adjustable
  42. // - code in bildschirmmitte (horizontal)?
  43. // - search in files of a directory
  44. // - Files in multiple tabs?
  45. // - Configurable tabulator display-width
  46.  
  47. interface
  48.  
  49. uses
  50.   // TODO: "{$IFDEF USE_SHDOCVW_TLB}_TLB{$ENDIF}" does not work with Delphi 10.2
  51.   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  52.   Dialogs, StdCtrls, OleCtrls, ComCtrls, ExtCtrls, ToolWin, IniFiles,
  53.   SynEditHighlighter, SynHighlighterPHP, SynEdit, ShDocVw, FindReplace,
  54.   ActnList, SynEditMiscClasses, SynEditSearch, RunPHP, ImgList, SynUnicode,
  55.   System.ImageList, System.Actions, Vcl.Menus, Vcl.Themes, System.UITypes,
  56.   SynEditCodeFolding;
  57.  
  58. {.$DEFINE OnlineHelp}
  59.  
  60. type
  61.   TForm1 = class(TForm)
  62.     PageControl1: TPageControl;
  63.     PlaintextTabSheet: TTabSheet;
  64.     HtmlTabSheet: TTabSheet;
  65.     Memo2: TMemo;
  66.     WebBrowser1: TWebBrowser;
  67.     Splitter1: TSplitter;
  68.     PageControl2: TPageControl;
  69.     CodeTabsheet: TTabSheet;
  70.     HelpTabsheet: TTabSheet;
  71.     WebBrowser2: TWebBrowser;
  72.     OpenDialog1: TOpenDialog;
  73.     Panel1: TPanel;
  74.     OpenDialog3: TOpenDialog;
  75.     SynEdit1: TSynEdit;
  76.     SynPHPSyn1: TSynPHPSyn;
  77.     Panel2: TPanel;
  78.     SynEditFocusTimer: TTimer;
  79.     Button1: TButton;
  80.     Button2: TButton;
  81.     Button3: TButton;
  82.     Button4: TButton;
  83.     Button5: TButton;
  84.     Button6: TButton;
  85.     ActionList: TActionList;
  86.     ActionFind: TAction;
  87.     ActionReplace: TAction;
  88.     ActionFindNext: TAction;
  89.     ActionGoto: TAction;
  90.     ActionSave: TAction;
  91.     ActionHelp: TAction;
  92.     ActionRun: TAction;
  93.     ActionESC: TAction;
  94.     Button7: TButton;
  95.     ActionOpen: TAction;
  96.     Button8: TButton;
  97.     Button9: TButton;
  98.     ActionFindPrev: TAction;
  99.     Timer1: TTimer;
  100.     ActionSpaceToTab: TAction;
  101.     Button11: TButton;
  102.     SynEditSearch1: TSynEditSearch;
  103.     TreeView1: TTreeView;
  104.     Splitter2: TSplitter;
  105.     btnLint: TButton;
  106.     ActionLint: TAction;
  107.     ImageList1: TImageList;
  108.     RunPopup: TPopupMenu;
  109.     OpeninIDE1: TMenuItem;
  110.     ActionRunConsole: TAction;
  111.     Runinconsole1: TMenuItem;
  112.     SavePopup: TPopupMenu;
  113.     Saveas1: TMenuItem;
  114.     Save1: TMenuItem;
  115.     SaveDialog1: TSaveDialog;
  116.     BtnSpecialChars: TImage;
  117.     BtnSpecialCharsOff: TImage;
  118.     BtnSpecialCharsOn: TImage;
  119.     BtnLightOn: TImage;
  120.     BtnLightOff: TImage;
  121.     BtnLight: TImage;
  122.     StartUpTimer: TTimer;
  123.     FileModTimer: TTimer;
  124.     GotoPHPdir1: TMenuItem;
  125.     PHPShell1: TMenuItem;
  126.     ActionSaveAs: TAction;
  127.     ActionGoToPHPDir: TAction;
  128.     ActionPHPInteractiveShell: TAction;
  129.     FontSizeTimer: TTimer;
  130.     procedure Run(Sender: TObject);
  131.     procedure RunConsole(Sender: TObject);
  132.     procedure FormShow(Sender: TObject);
  133.     procedure FormCreate(Sender: TObject);
  134.     procedure FormDestroy(Sender: TObject);
  135.     procedure FormClose(Sender: TObject; var Action: TCloseAction);
  136.     procedure PageControl2Changing(Sender: TObject; var AllowChange: Boolean);
  137.     procedure Memo2DblClick(Sender: TObject);
  138.     (*
  139.     {$IFDEF USE_SHDOCVW_TLB}
  140.     *)
  141.     procedure WebBrowser1BeforeNavigate2(ASender: TObject;
  142.       const pDisp: IDispatch; const URL, Flags, TargetFrameName, PostData,
  143.       Headers: OleVariant; var Cancel: WordBool);
  144.     (*
  145.     {$ELSE}
  146.     procedure WebBrowser1BeforeNavigate2(ASender: TObject;
  147.       const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
  148.       Headers: OleVariant; var Cancel: WordBool);
  149.     {$ENDIF}
  150.     *)
  151.     procedure BeforeNavigate(const URL: OleVariant; var Cancel: WordBool);
  152.     procedure SynEditFocusTimerTimer(Sender: TObject);
  153.     procedure ActionFindExecute(Sender: TObject);
  154.     procedure ActionReplaceExecute(Sender: TObject);
  155.     procedure ActionFindNextExecute(Sender: TObject);
  156.     procedure ActionGotoExecute(Sender: TObject);
  157.     procedure ActionSaveExecute(Sender: TObject);
  158.     procedure ActionHelpExecute(Sender: TObject);
  159.     procedure ActionRunExecute(Sender: TObject);
  160.     procedure ActionESCExecute(Sender: TObject);
  161.     procedure SynEdit1MouseWheelDown(Sender: TObject; Shift: TShiftState;
  162.       MousePos: TPoint; var Handled: Boolean);
  163.     procedure SynEdit1MouseWheelUp(Sender: TObject; Shift: TShiftState;
  164.       MousePos: TPoint; var Handled: Boolean);
  165.     procedure ActionOpenExecute(Sender: TObject);
  166.     procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  167.     procedure Memo2KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  168.     procedure ActionFindPrevExecute(Sender: TObject);
  169.     procedure SynEdit1MouseCursor(Sender: TObject;
  170.       const aLineCharPos: TBufferCoord; var aCursor: TCursor);
  171.     procedure Timer1Timer(Sender: TObject);
  172.     procedure ActionSpaceToTabExecute(Sender: TObject);
  173.     procedure TreeView1DblClick(Sender: TObject);
  174.     procedure SynEdit1GutterClick(Sender: TObject; Button: TMouseButton; X, Y,
  175.       Line: Integer; Mark: TSynEditMark);
  176.     procedure SynEdit1PaintTransient(Sender: TObject; Canvas: TCanvas;
  177.       TransientType: TTransientType);
  178.     procedure ActionLintExecute(Sender: TObject);
  179.     procedure ActionRunConsoleExecute(Sender: TObject);
  180.     procedure SynEdit1Change(Sender: TObject);
  181.     procedure BtnSpecialCharsClick(Sender: TObject);
  182.     procedure WebBrowser1WindowClosing(ASender: TObject;
  183.       IsChildWindow: WordBool; var Cancel: WordBool);
  184.     procedure BtnLightClick(Sender: TObject);
  185.     procedure StartUpTimerTimer(Sender: TObject);
  186.     procedure FileModTimerTimer(Sender: TObject);
  187.     procedure SynEdit1DropFiles(Sender: TObject; X, Y: Integer;
  188.       AFiles: TStrings);
  189.     procedure ActionSaveAsExecute(Sender: TObject);
  190.     procedure ActionGoToPHPDirExecute(Sender: TObject);
  191.     procedure ActionPHPInteractiveShellExecute(Sender: TObject);
  192.     procedure SynEdit1KeyDown(Sender: TObject; var Key: Word;
  193.       Shift: TShiftState);
  194.     procedure FontSizeTimerTimer(Sender: TObject);
  195.   private
  196.     hMutex: THandle;
  197.     CurSearchTerm: string;
  198.     HlpPrevPageIndex: integer;
  199.     SrcRep: TSynEditFindReplace;
  200.     {$IFDEF OnlineHelp}
  201.     gOnlineHelpWord: string;
  202.     {$ENDIF}
  203.     FileModLast: TDateTime;
  204.     FormShowRanOnce: boolean;
  205.     BrowserLoadedOnce: boolean;
  206.     procedure Help;
  207.     function InputRequestCallback(var data: AnsiString): boolean;
  208.     function OutputNotifyCallback(const data: AnsiString): boolean;
  209.     procedure RightTrimAll;
  210.   protected
  211.     ChmIndex: TMemIniFile;
  212.     FScrapFile: string;
  213.     FSaveAsFilename: string;
  214.     codeExplorer: TRunCodeExplorer;
  215.     procedure GotoLineNo(LineNo: integer);
  216.     function GetScrapFile: string;
  217.     procedure StartCodeExplorer;
  218.     procedure RefreshModifySign;
  219.     procedure Theme_Light;
  220.     procedure Theme_Dark;
  221.     function IsThemeDark: boolean;
  222.     function MarkUpLineReference(cont: string): string;
  223.     function ContainsUnicodeCharsInAnsiFile: boolean;
  224.     procedure SaveToFile(filename: string);
  225.   end;
  226.  
  227. var
  228.   Form1: TForm1;
  229.  
  230. implementation
  231.  
  232. {$R *.dfm}
  233.  
  234. {$R Cursors.res}
  235.  
  236. uses
  237.   Functions, StrUtils, WebBrowserUtils, FastPHPUtils, Math, ShellAPI, RichEdit,
  238.   FastPHPTreeView, ImageListEx, FastPHPConfig;
  239.  
  240. const
  241.   crMouseGutter = 1;
  242.  
  243. procedure TForm1.RefreshModifySign;
  244. var
  245.   tmp: string;
  246. begin
  247.   tmp := Caption;
  248.  
  249.   tmp := StringReplace(tmp, '*', '', [rfReplaceAll]);
  250.   if SynEdit1.Modified then tmp := tmp + '*';
  251.  
  252.   if Caption <> tmp then Caption := tmp;
  253. end;
  254.  
  255. procedure TForm1.ActionFindNextExecute(Sender: TObject);
  256. begin
  257.   SrcRep.FindNext;
  258. end;
  259.  
  260. procedure TForm1.ActionFindPrevExecute(Sender: TObject);
  261. begin
  262.   SrcRep.FindPrev;
  263. end;
  264.  
  265. procedure TForm1.ActionGotoExecute(Sender: TObject);
  266. var
  267.   val: string;
  268.   lineno: integer;
  269. resourcestring
  270.   SGoTo = 'Go to';
  271.   SLineNumber = 'Line number:';
  272. begin
  273.   // TODO: VK_LMENU does not work! only works with AltGr but not Alt
  274.   // http://stackoverflow.com/questions/16828250/delphi-xe2-how-to-prevent-the-alt-key-stealing-focus ?
  275.  
  276.   if not InputQuery(SGoTo, SLineNumber, val) or
  277.      not TryStrToInt(val, lineno) then
  278.   begin
  279.     if SynEdit1.CanFocus then SynEdit1.SetFocus;
  280.     exit;
  281.   end;
  282.   GotoLineNo(lineno);
  283. end;
  284.  
  285. procedure TForm1.ActionGoToPHPDirExecute(Sender: TObject);
  286. var
  287.   phpExe: string;
  288. begin
  289.   phpExe := GetPHPExe;
  290.   if phpExe <> '' then
  291.     ShellExecute(Handle, 'open', 'explorer.exe', PChar(ExtractFilePath(phpExe)), '', SW_NORMAL);
  292. end;
  293.  
  294. procedure TForm1.ActionHelpExecute(Sender: TObject);
  295. begin
  296.   Help;
  297.   if PageControl2.ActivePage = HelpTabsheet then
  298.     WebBrowser2.SetFocus
  299.   else if PageControl2.ActivePage = CodeTabsheet then
  300.     SynEdit1.SetFocus;
  301. end;
  302.  
  303. procedure TForm1.ActionLintExecute(Sender: TObject);
  304. begin
  305.   Run(Sender);
  306.   SynEdit1.SetFocus;
  307. end;
  308.  
  309. procedure TForm1.ActionOpenExecute(Sender: TObject);
  310. begin
  311.   If OpenDialog3.Execute then
  312.   begin
  313.     ShellExecute(0, 'open', PChar(ParamStr(0)), PChar('"' + OpenDialog3.FileName + '"'), '', SW_NORMAL);
  314.   end;
  315. end;
  316.  
  317. procedure TForm1.ActionPHPInteractiveShellExecute(Sender: TObject);
  318. var
  319.   phpExe: string;
  320. begin
  321.   phpExe := GetPHPExe;
  322.   if phpExe <> '' then
  323.     ShellExecute(Handle, 'open', PChar(phpExe), '-a', PChar(ExtractFilePath(phpExe)), SW_NORMAL);
  324. end;
  325.  
  326. procedure TForm1.ActionReplaceExecute(Sender: TObject);
  327. begin
  328.   SrcRep.ReplaceExecute;
  329. end;
  330.  
  331. procedure TForm1.ActionRunConsoleExecute(Sender: TObject);
  332. begin
  333.   RunConsole(Sender);
  334.   SynEdit1.SetFocus;
  335. end;
  336.  
  337. procedure TForm1.ActionRunExecute(Sender: TObject);
  338. begin
  339.   Run(Sender);
  340.   SynEdit1.SetFocus;
  341. end;
  342.  
  343. procedure TForm1.RightTrimAll;
  344. var
  345.   i: integer;
  346. begin
  347.   for i := 0 to SynEdit1.Lines.Count-1 do
  348.   begin
  349.     SynEdit1.Lines.Strings[i] := TrimRight(SynEdit1.Lines.Strings[i]);
  350.   end;
  351.  
  352.   (*
  353.   while (SynEdit1.Lines.Count > 0) and (SynEdit1.Lines.Strings[SynEdit1.Lines.Count-1] = '') do
  354.   begin
  355.     SynEdit1.Lines.Delete(SynEdit1.Lines.Count-1);
  356.   end;
  357.   if SynEdit1.SelStart > Length(SynEdit1.Text)-1 then
  358.   begin
  359.     // TODO: This code does not work...
  360.     SynEdit1.SelStart := Length(SynEdit1.Text)-1;
  361.     SynEdit1.SelEnd   := Length(SynEdit1.Text)-1;
  362.   end;
  363.   *)
  364. end;
  365.  
  366. procedure TForm1.ActionSaveAsExecute(Sender: TObject);
  367. var
  368.   hMutexNew: THandle;
  369. resourcestring
  370.   SCannotSaveBecauseMutex = 'Cannot save because file "%s", because it is alrady open in another FastPHP window!';
  371. begin
  372.   if SaveDialog1.Execute then
  373.   begin
  374.     {$REGION 'Switch mutex'}
  375.     hMutexNew := CreateMutex(nil, True, PChar('FastPHP'+md5(UpperCase(SaveDialog1.FileName))));
  376.     if GetLastError = ERROR_ALREADY_EXISTS then
  377.     begin
  378.       ShowMessageFmt(SCannotSaveBecauseMutex, [SaveDialog1.FileName]);
  379.       Close;
  380.     end;
  381.  
  382.     if hMutex <> 0 then CloseHandle(hMutex); // Note: ReleaseMutex does not work as expected!
  383.     hMutex := hMutexNew;
  384.     {$ENDREGION}
  385.  
  386.     FSaveAsFilename := SaveDialog1.FileName;
  387.     Caption := Copy(Caption, 1, Pos(' - ', Caption)-1) + ' - ' + FSaveAsFilename;
  388.     Application.Title := Format('%s - FastPHP', [ExtractFileName(FSaveAsFilename)]); // do not translate!
  389.     Button7.Click;
  390.   end;
  391. end;
  392.  
  393. procedure TForm1.ActionSaveExecute(Sender: TObject);
  394. begin
  395.   RightTrimAll;
  396.   SaveToFile(GetScrapFile);
  397.   SynEdit1.Modified := false;
  398.   RefreshModifySign;
  399.   if SynEdit1.CanFocus then SynEdit1.SetFocus;
  400. end;
  401.  
  402. procedure TForm1.ActionSpaceToTabExecute(Sender: TObject);
  403.  
  404.     function SpacesAtBeginning(line: string): integer;
  405.     begin
  406.       result := 0;
  407.       if Trim(line) = '' then exit;
  408.       while line[result+1] = ' ' do
  409.       begin
  410.         inc(result);
  411.       end;
  412.     end;
  413.  
  414.     function GuessIndent(lines: {$IFDEF UNICODE}TStrings{$ELSE}TUnicodeStrings{$ENDIF}): integer;
  415.       function _Check(indent: integer): boolean;
  416.       var
  417.         i: integer;
  418.       begin
  419.         result := true;
  420.         for i := 0 to lines.Count-1 do
  421.           if SpacesAtBeginning(lines.Strings[i]) mod indent <> 0 then
  422.           begin
  423.             // ShowMessageFmt('Zeile "%s" nicht durch %d teilbar!', [lines.strings[i], indent]);
  424.             result := false;
  425.             exit;
  426.           end;
  427.       end;
  428.     var
  429.       i: integer;
  430.     begin
  431.       for i := 8 downto 2 do
  432.       begin
  433.         if _Check(i) then
  434.         begin
  435.           result := i;
  436.           exit;
  437.         end;
  438.       end;
  439.       result := -1;
  440.     end;
  441.  
  442.     procedure SpaceToTab(lines: {$IFDEF UNICODE}TStrings{$ELSE}TUnicodeStrings{$ENDIF}; indent: integer);
  443.     var
  444.       i, spaces: integer;
  445.       newval: string;
  446.       somethingchanged: boolean;
  447.     begin
  448.       somethingchanged := false;
  449.       for i := 0 to lines.Count-1 do
  450.       begin
  451.         spaces := SpacesAtBeginning(lines.Strings[i]);
  452.         newval := StringOfChar(#9, spaces div indent) + StringOfChar(' ', spaces mod indent) + Copy(lines.Strings[i], spaces+1, Length(lines.Strings[i])-spaces);
  453.         if lines.Strings[i] <> newval then
  454.         begin
  455.           somethingchanged := true;
  456.           lines.Strings[i] := newval;
  457.         end;
  458.       end;
  459.       if somethingchanged then
  460.       begin
  461.         SynEdit1Change(SynEdit1); // set the "changed" flag
  462.       end;
  463.     end;
  464.  
  465.     function SpacesAvailable(lines: {$IFDEF UNICODE}TStrings{$ELSE}TUnicodeStrings{$ENDIF}): boolean;
  466.     var
  467.       i, spaces: integer;
  468.     begin
  469.       for i := 0 to lines.Count-1 do
  470.       begin
  471.         spaces := SpacesAtBeginning(lines.Strings[i]);
  472.         if spaces > 0 then
  473.         begin
  474.           result := true;
  475.           exit;
  476.         end;
  477.       end;
  478.       result := false;
  479.       exit;
  480.     end;
  481.  
  482. var
  483.   val: string;
  484.   ind: integer;
  485. resourcestring
  486.   SNoLinesAvailable = 'No lines with spaces at the beginning available';
  487.   SSpacesToLabs = 'Spaces to tabs';
  488.   SIndent = 'Indent:';
  489. begin
  490.   // TODO: if something is selected, only process the selected part
  491.  
  492.   if not SpacesAvailable(SynEdit1.Lines) then
  493.   begin
  494.     MessageDlg(SNoLinesAvailable, mtInformation, [mbOk], 0);
  495.     exit;
  496.   end;
  497.  
  498.   ind := GuessIndent(SynEdit1.Lines);
  499.   if ind <> -1 then val := IntToStr(ind);
  500.  
  501.   if not InputQuery(SSpacesToLabs, SIndent, val) or
  502.      not TryStrToInt(Trim(val), ind) then
  503.   begin
  504.     if SynEdit1.CanFocus then SynEdit1.SetFocus;
  505.     exit;
  506.   end;
  507.  
  508.   if ind = 0 then exit;
  509.   SpaceToTab(SynEdit1.Lines, ind);
  510. end;
  511.  
  512. procedure TForm1.ActionESCExecute(Sender: TObject);
  513. begin
  514.   if (HlpPrevPageIndex <> -1) and (PageControl2.ActivePage = HelpTabSheet) and
  515.      (HelpTabsheet.TabVisible) then
  516.   begin
  517.     PageControl2.ActivePageIndex := HlpPrevPageIndex;
  518.     HelpTabsheet.TabVisible := false;
  519.   end;
  520.  
  521.   // Dirty hack...
  522.   SrcRep.CloseDialogs;
  523. end;
  524.  
  525. procedure TForm1.ActionFindExecute(Sender: TObject);
  526. begin
  527.   SrcRep.FindExecute;
  528. end;
  529.  
  530. procedure TForm1.Run(Sender: TObject);
  531. var
  532.   bakTS: TTabSheet;
  533.   //ss: TStringStream;
  534.   //bakPos: Int64;
  535. begin
  536.   memo2.Lines.Text := '';
  537.  
  538.   if not BrowserLoadedOnce then
  539.   begin
  540.     bakTS := PageControl1.ActivePage;
  541.     try
  542.       PageControl1.ActivePage := HtmlTabSheet; // Required for the first time, otherwise, WebBrowser1.Clear will hang
  543.       Webbrowser1.Clear;
  544.     finally
  545.       PageControl1.ActivePage := bakTS;
  546.     end;
  547.     BrowserLoadedOnce := true;
  548.   end
  549.   else
  550.     Webbrowser1.Clear;
  551.  
  552.   Screen.Cursor := crHourGlass; // TODO: Doesn't work with F9
  553.   Application.ProcessMessages;
  554.  
  555.   try
  556.     ActionSave.Execute; // TODO: if it is not the scrap file: do not save the file, since the user did not intended to save... better create a temporary file and run it instead.
  557.  
  558.     // TODO 70421 * <fastphp> implement flush() with ContentCallBack implementieren... For long running scripts I want to see status changes via javascript which are loaded step by step
  559.     // TODO 70422 * <fastphp> when a script has an endless loop, i want to have a possibility to cancel it
  560.     if SynEdit1.Lines.Encoding = TEncoding.UTF8 then
  561.       memo2.Lines.Text := Utf8Decode(RunPHPScript(GetScrapFile, Sender=ActionLint, False)) // if we have a UTF-8 file, then the DOS output is double-UTF8 encoded
  562.     else
  563.       memo2.Lines.Text := RunPHPScript(GetScrapFile, Sender=ActionLint, False);
  564.  
  565.     {$REGION 'Show in Web Browser'}
  566.     if SynEdit1.Lines.Encoding = TEncoding.UTF8 then
  567.       Webbrowser1.LoadHTML(MarkUpLineReference('<meta charset="utf-8">'+memo2.Lines.Text), GetScrapFile)
  568.     else
  569.       Webbrowser1.LoadHTML(MarkUpLineReference(memo2.Lines.Text), GetScrapFile);
  570.  
  571.     // Alternatively:
  572.     (*
  573.     ss := TstringStream.Create;
  574.     ss.WriteString(MarkUpLineReference(memo2.Lines.Text));
  575.     ss.Position := 0;
  576.     Webbrowser1.LoadStream(ss, GetScrapFile);
  577.     Webbrowser1.Wait;
  578.     ss.Free;
  579.     *)
  580.     {$ENDREGION}
  581.  
  582.     if IsTextHTML(memo2.lines.text) then
  583.       PageControl1.ActivePage := HtmlTabSheet
  584.     else
  585.       PageControl1.ActivePage := PlaintextTabSheet;
  586.   finally
  587.     Screen.Cursor := crDefault;
  588.   end;
  589. end;
  590.  
  591. procedure TForm1.RunConsole(Sender: TObject);
  592. begin
  593.   ActionSave.Execute; // TODO: if it is not the scrap file: do not save the file, since the user did not intended to save... better create a temporary file and run it instead.
  594.   RunPHPScript(GetScrapFile, Sender=ActionLint, True);
  595. end;
  596.  
  597. procedure TForm1.SynEdit1Change(Sender: TObject);
  598. begin
  599.   RefreshModifySign;
  600. end;
  601.  
  602. procedure TForm1.SynEdit1DropFiles(Sender: TObject; X, Y: Integer;
  603.   AFiles: TStrings);
  604. var
  605.   FileName: string;
  606. const
  607.   WARN_FILE_COUNT = 10;
  608. resourcestring
  609.   SAreYouSure = 'Are you sure you want to open %d files?';
  610. begin
  611.   if AFiles.Count > WARN_FILE_COUNT then
  612.   begin
  613.     if not MessageDlg(Format(SAreYouSure, [WARN_FILE_COUNT]), mtConfirmation, mbYesNoCancel, 0) <> mrYes then
  614.       exit;
  615.   end;
  616.  
  617.   for FileName in AFiles do
  618.   begin
  619.     ShellExecute(0, 'open', PChar(ParamStr(0)), PChar('"' + FileName + '"'), '', SW_NORMAL);
  620.   end;
  621. end;
  622.  
  623. procedure TForm1.SynEdit1GutterClick(Sender: TObject; Button: TMouseButton; X,
  624.   Y, Line: Integer; Mark: TSynEditMark);
  625. begin
  626.   (*
  627.   TSynEdit(Sender).CaretX := 1;
  628.   TSynEdit(Sender).CaretY := Line;
  629.   TSynEdit(Sender).SelLength := Length(TSynEdit(Sender).LineText);
  630.   *)
  631. end;
  632.  
  633. procedure TForm1.SynEdit1KeyDown(Sender: TObject; var Key: Word;
  634.   Shift: TShiftState);
  635. begin
  636.   if (Shift = [ssCtrl, ssShift]) and (Key = ord('C')) then
  637.   begin
  638.     // Disable "Column Select Mode" (https://github.com/SynEdit/SynEdit/blob/master/Source/SynEditKeyCmds.pas#L879)
  639.     // which can be enabled by pressing Ctrl+Shift+C
  640.     // Reasons why we disable it:
  641.     // 1. I think nobody needs this, and sometimes you accidentally press it
  642.     // 2. Ctrl+Shift+L would the combination to disable column select mode,
  643.     //    but you cannot use it, because it is already in use for linting
  644.     Key := 0;
  645.   end;
  646. end;
  647.  
  648. procedure TForm1.SynEdit1MouseCursor(Sender: TObject; const aLineCharPos: TBufferCoord; var aCursor: TCursor);
  649. {$IFDEF OnlineHelp}
  650. var
  651.   Line: Integer;
  652.   Column: Integer;
  653.   word: string;
  654. begin
  655.   Line  := aLineCharPos.Line-1;
  656.   Column := aLineCharPos.Char-1;
  657.   word := GetWordUnderPos(TSynEdit(Sender), Line, Column);
  658.   if word <> gOnlineHelpWord then
  659.   begin
  660.     gOnlineHelpWord := word;
  661.     Timer1.Enabled := false;
  662.     Timer1.Enabled := true;
  663.   end;
  664. {$ELSE}
  665. begin
  666. {$ENDIF}
  667. end;
  668.  
  669. procedure TForm1.SynEdit1MouseWheelDown(Sender: TObject; Shift: TShiftState;
  670.   MousePos: TPoint; var Handled: Boolean);
  671. begin
  672.   if ssCtrl in Shift then
  673.   begin
  674.     SynEdit1.Font.Size := Max(SynEdit1.Font.Size - 1, 5);
  675.     Handled := true;
  676.   end
  677.   else Handled := false;
  678. end;
  679.  
  680. procedure TForm1.SynEdit1MouseWheelUp(Sender: TObject; Shift: TShiftState;
  681.   MousePos: TPoint; var Handled: Boolean);
  682. begin
  683.   if ssCtrl in Shift then
  684.   begin
  685.     SynEdit1.Font.Size := SynEdit1.Font.Size + 1;
  686.     Handled := true;
  687.   end
  688.   else Handled := false;
  689. end;
  690.  
  691. procedure TForm1.SynEdit1PaintTransient(Sender: TObject; Canvas: TCanvas; TransientType: TTransientType);
  692. var
  693.   Editor: TSynEdit;
  694.   OpenChars: array of WideChar;//[0..2] of WideChar=();
  695.   CloseChars: array of WideChar;//[0..2] of WideChar=();
  696.  
  697.   function IsCharBracket(AChar: WideChar): Boolean;
  698.   begin
  699.     case AChar of
  700.       '{','[','(','<','}',']',')','>':
  701.         Result := True;
  702.       else
  703.         Result := False;
  704.     end;
  705.   end;
  706.  
  707.   function CharToPixels(P: TBufferCoord): TPoint;
  708.   begin
  709.     Result := Editor.RowColumnToPixels(Editor.BufferToDisplayPos(P));
  710.   end;
  711.  
  712. var
  713.   COLOR_FG: TColor;
  714.   COLOR_BG: TColor;
  715.   P: TBufferCoord;
  716.   Pix: TPoint;
  717.   D: TDisplayCoord;
  718.   S: UnicodeString;
  719.   I: Integer;
  720.   Attri: TSynHighlighterAttributes;
  721.   ArrayLength: Integer;
  722.   start: Integer;
  723.   TmpCharA, TmpCharB: WideChar;
  724. begin
  725.   // Source: https://github.com/SynEdit/SynEdit/blob/master/Demos/OnPaintTransientDemo/Unit1.pas
  726.  
  727.   if IsThemeDark then
  728.   begin
  729.     COLOR_FG := clLime;
  730.     COLOR_BG := clGreen;
  731.   end
  732.   else
  733.   begin
  734.     COLOR_FG := clGreen;
  735.     COLOR_BG := clLime;
  736.   end;
  737.  
  738.   if TSynEdit(Sender).SelAvail then exit;
  739.   Editor := TSynEdit(Sender);
  740.   ArrayLength:= 3;
  741.  
  742.   (*
  743.   if (Editor.Highlighter = shHTML) or (Editor.Highlighter = shXML) then
  744.     inc(ArrayLength);
  745.   *)
  746.  
  747.   SetLength(OpenChars, ArrayLength);
  748.   SetLength(CloseChars, ArrayLength);
  749.   for i := 0 to ArrayLength - 1 do
  750.   begin
  751.     case i of
  752.       0: begin OpenChars[i] := '('; CloseChars[i] := ')'; end;
  753.       1: begin OpenChars[i] := '{'; CloseChars[i] := '}'; end;
  754.       2: begin OpenChars[i] := '['; CloseChars[i] := ']'; end;
  755.       3: begin OpenChars[i] := '<'; CloseChars[i] := '>'; end;
  756.     end;
  757.   end;
  758.  
  759.   P := Editor.CaretXY;
  760.   D := Editor.DisplayXY;
  761.  
  762.   Start := Editor.SelStart;
  763.  
  764.   if (Start > 0) and (Start <= length(Editor.Text)) then
  765.     TmpCharA := Editor.Text[Start]
  766.   else
  767.     TmpCharA := #0;
  768.  
  769.   if (Start > 0){Added by VTS} and (Start < length(Editor.Text)) then
  770.     TmpCharB := Editor.Text[Start + 1]
  771.   else
  772.     TmpCharB := #0;
  773.  
  774.   if not IsCharBracket(TmpCharA) and not IsCharBracket(TmpCharB) then exit;
  775.   S := TmpCharB;
  776.   if not IsCharBracket(TmpCharB) then
  777.   begin
  778.     P.Char := P.Char - 1;
  779.     S := TmpCharA;
  780.   end;
  781.   Editor.GetHighlighterAttriAtRowCol(P, S, Attri);
  782.  
  783.   if (Editor.Highlighter.SymbolAttribute = Attri) then
  784.   begin
  785.     for i := low(OpenChars) to High(OpenChars) do
  786.     begin
  787.       if (S = OpenChars[i]) or (S = CloseChars[i]) then
  788.       begin
  789.         Pix := CharToPixels(P);
  790.  
  791.         Editor.Canvas.Brush.Style := bsSolid;//Clear;
  792.         Editor.Canvas.Font.Assign(Editor.Font);
  793.         Editor.Canvas.Font.Style := Attri.Style;
  794.  
  795.         if (TransientType = ttAfter) then
  796.         begin
  797.           Editor.Canvas.Font.Color := COLOR_FG;
  798.           Editor.Canvas.Brush.Color := COLOR_BG;
  799.         end
  800.         else
  801.         begin
  802.           Editor.Canvas.Font.Color := Attri.Foreground;
  803.           Editor.Canvas.Brush.Color := Attri.Background;
  804.         end;
  805.         if Editor.Canvas.Font.Color = clNone then
  806.           Editor.Canvas.Font.Color := Editor.Font.Color;
  807.         if Editor.Canvas.Brush.Color = clNone then
  808.           Editor.Canvas.Brush.Color := Editor.Color;
  809.  
  810.         Editor.Canvas.TextOut(Pix.X, Pix.Y, S);
  811.         P := Editor.GetMatchingBracketEx(P);
  812.  
  813.         if (P.Char > 0) and (P.Line > 0) then
  814.         begin
  815.           Pix := CharToPixels(P);
  816.           if Pix.X > Editor.Gutter.Width then
  817.           begin
  818.             {$REGION 'Added by ViaThinkSoft'}
  819.             if (TransientType = ttAfter) then
  820.             begin
  821.               Editor.Canvas.Font.Color := COLOR_FG;
  822.               Editor.Canvas.Brush.Color := COLOR_BG;
  823.             end
  824.             else
  825.             begin
  826.               Editor.Canvas.Font.Color := Attri.Foreground;
  827.               Editor.Canvas.Brush.Color := Attri.Background;
  828.             end;
  829.             if Editor.Canvas.Font.Color = clNone then
  830.               Editor.Canvas.Font.Color := Editor.Font.Color;
  831.             if Editor.Canvas.Brush.Color = clNone then
  832.               Editor.Canvas.Brush.Color := Editor.Color;
  833.             {$ENDREGION}
  834.             if S = OpenChars[i] then
  835.               Editor.Canvas.TextOut(Pix.X, Pix.Y, CloseChars[i])
  836.             else Editor.Canvas.TextOut(Pix.X, Pix.Y, OpenChars[i]);
  837.           end;
  838.         end;
  839.       end;
  840.     end;
  841.     Editor.Canvas.Brush.Style := bsSolid;
  842.   end;
  843. end;
  844.  
  845. procedure TForm1.SynEditFocusTimerTimer(Sender: TObject);
  846. begin
  847.   SynEditFocusTimer.Enabled := false;
  848.   Button1.SetFocus; // Workaround for weird bug... This (and the timer) is necessary to get the focus to SynEdit1
  849.   SynEdit1.SetFocus;
  850. end;
  851.  
  852. procedure TForm1.Theme_Dark;
  853. begin
  854.   if IsThemeDark then exit;
  855.   TStyleManager.TrySetStyle('Windows10 SlateGray'); // do not translate
  856.   Color := 1316887;
  857.   Font.Color := clCream;
  858.   //Memo2.Font.Color := clCream;
  859.   //Memo2.ParentColor := true;
  860.   SynEdit1.ActiveLineColor := 2238502;
  861.   SynEdit1.Color := 1316887;
  862.   SynEdit1.Font.Color := clCream;
  863.   SynEdit1.Gutter.Color := 1316887;
  864.   SynEdit1.Gutter.Font.Color := clCream;
  865.   SynEdit1.Gutter.GradientStartColor := 2238502;
  866.   SynEdit1.Gutter.GradientEndColor := 1316887;
  867.   SynPHPSyn1.CommentAttri.Foreground := $00837B82;
  868.   SynPHPSyn1.IdentifierAttri.Foreground := 9627120;
  869.   SynPHPSyn1.KeyAttri.Foreground := 4157595;
  870.   SynPHPSyn1.NumberAttri.Foreground := 5008079;
  871.   SynPHPSyn1.StringAttri.Foreground := 6987151;
  872.   SynPHPSyn1.SymbolAttri.Foreground := 8769754;
  873.   SynPHPSyn1.VariableAttri.Foreground := 6924493;
  874. end;
  875.  
  876. procedure TForm1.Theme_Light;
  877. begin
  878.   if not IsThemeDark then exit;
  879.   TStyleManager.TrySetStyle('Windows'); // do not translate
  880.   Color := clBtnFace;
  881.   Font.Color := clWindowText;
  882.   //Memo2.Font.Color := clWindowText;
  883.   SynEdit1.ActiveLineColor := 14680010;
  884.   SynEdit1.Color := clCream;
  885.   SynEdit1.Font.Color := clWindowText;
  886.   SynEdit1.Gutter.Color := clBtnFace;
  887.   SynEdit1.Gutter.Font.Color := clWindowText;
  888.   SynEdit1.Gutter.GradientStartcolor := cl3dLight;
  889.   SynEdit1.Gutter.GradientEndColor := clBtnFace;;
  890.   SynPHPSyn1.CommentAttri.Foreground := 33023;
  891.   SynPHPSyn1.IdentifierAttri.Foreground := 4194304;
  892.   SynPHPSyn1.KeyAttri.Foreground := 4227072;
  893.   SynPHPSyn1.NumberAttri.Foreground := 213;
  894.   SynPHPSyn1.StringAttri.Foreground := 13762560;
  895.   SynPHPSyn1.SymbolAttri.Foreground := 4227072;
  896.   SynPHPSyn1.VariableAttri.Foreground := 213;
  897. end;
  898.  
  899. procedure TForm1.Timer1Timer(Sender: TObject);
  900. begin
  901.   {$IFDEF OnlineHelp}
  902.   Timer1.Enabled := false;
  903.  
  904.   // TODO: Insert a small online help hint
  905.   //Caption := gOnlineHelpWord;
  906.   {$ENDIF}
  907. end;
  908.  
  909. procedure TForm1.TreeView1DblClick(Sender: TObject);
  910. var
  911.   tn: TTreeNode;
  912.   lineNo: integer;
  913. begin
  914.   tn := TTreeView(Sender).Selected;
  915.   if tn = nil then exit;
  916.   lineNo := Integer(tn.Data);
  917.   if lineNo > 0 then GotoLineNo(lineNo);
  918. end;
  919.  
  920. (*
  921. {$IFDEF USE_SHDOCVW_TLB}
  922. *)
  923. procedure TForm1.WebBrowser1BeforeNavigate2(ASender: TObject;
  924.   const pDisp: IDispatch; const URL, Flags, TargetFrameName, PostData,
  925.   Headers: OleVariant; var Cancel: WordBool);
  926. begin
  927.   BeforeNavigate(URL, Cancel);
  928. end;
  929. (*
  930. {$ELSE}
  931. procedure TForm1.WebBrowser1BeforeNavigate2(ASender: TObject;
  932.   const pDisp: IDispatch; var URL, Flags, TargetFrameName, PostData,
  933.   Headers: OleVariant; var Cancel: WordBool);
  934. begin
  935.   BeforeNavigate(URL, Cancel);
  936. end;
  937. {$ENDIF}
  938. *)
  939.  
  940. procedure TForm1.WebBrowser1WindowClosing(ASender: TObject;
  941.   IsChildWindow: WordBool; var Cancel: WordBool);
  942. resourcestring
  943.   SCloseRequest = 'A script has requested the window to be closed. The window of a standalone script would now close.';
  944. begin
  945.   ShowMessage(SCloseRequest);
  946.   TWebBrowser(ASender).Clear;
  947.   Cancel := true;
  948. end;
  949.  
  950. procedure TForm1.BeforeNavigate(const URL: OleVariant; var Cancel: WordBool);
  951. var
  952.   s, myURL: string;
  953.   lineno: integer;
  954.   p: integer;
  955. begin
  956.   {$REGION 'Line number references (PHP errors and warnings)'}
  957.   if Copy(URL, 1, length(FASTPHP_GOTO_URI_PREFIX)) = FASTPHP_GOTO_URI_PREFIX then
  958.   begin
  959.     try
  960.       s := copy(URL, length(FASTPHP_GOTO_URI_PREFIX)+1, 99);
  961.       if not TryStrToInt(s, lineno) then exit;
  962.       GotoLineNo(lineno);
  963.       SynEditFocusTimer.Enabled := true;
  964.     finally
  965.       Cancel := true;
  966.     end;
  967.     Exit;
  968.   end;
  969.   {$ENDREGION}
  970.  
  971.   {$REGION 'Intelligent browser (executes PHP scripts which are clicked in a hyperlink)'}
  972.   if URL <> 'about:blank' then
  973.   begin
  974.     myUrl := URL;
  975.  
  976.     p := Pos('?', myUrl);
  977.     if p >= 1 then myURL := copy(myURL, 1, p-1);
  978.  
  979.     // TODO: myURL urldecode
  980.     // TODO: maybe we could even open that file in the editor!
  981.     // TODO: ?parameter=....
  982.  
  983.     if FileExists(myURL) and (EndsText('.php', myURL) or EndsText('.php3', myURL) or EndsText('.php4', myURL) or EndsText('.php5', myURL) or EndsText('.phps', myURL)) then
  984.     begin
  985.       WebBrowser1.LoadHTML(RunPHPScript(myURL), myUrl);
  986.       Cancel := true;
  987.     end;
  988.   end;
  989.   {$ENDREGION}
  990. end;
  991.  
  992. procedure TForm1.BtnLightClick(Sender: TObject);
  993. var
  994.   CanClose: boolean;
  995. begin
  996.   FormCloseQuery(Form1, CanClose);
  997.   if not CanClose then exit;
  998.  
  999.   if IsThemeDark then
  1000.   begin
  1001.     BtnLight.Picture.Assign(BtnLightOn.Picture);
  1002.     Theme_Light;
  1003.     TFastPHPConfig.DarkTheme := false;
  1004.   end
  1005.   else
  1006.   begin
  1007.     BtnLight.Picture.Assign(BtnLightOff.Picture);
  1008.     Theme_Dark;
  1009.     TFastPHPConfig.DarkTheme := true;
  1010.   end;
  1011. end;
  1012.  
  1013. procedure TForm1.BtnSpecialCharsClick(Sender: TObject);
  1014. var
  1015.   opts: TSynEditorOptions;
  1016. begin
  1017.   opts := SynEdit1.Options;
  1018.   if eoShowSpecialChars in SynEdit1.Options then
  1019.   begin
  1020.     BtnSpecialChars.Picture.Assign(BtnSpecialCharsOff.Picture);
  1021.     Exclude(opts, eoShowSpecialChars);
  1022.     TFastPHPConfig.SpecialChars := false;
  1023.   end
  1024.   else
  1025.   begin
  1026.     BtnSpecialChars.Picture.Assign(BtnSpecialCharsOn.Picture);
  1027.     Include(opts, eoShowSpecialChars);
  1028.     TFastPHPConfig.SpecialChars := true;
  1029.   end;
  1030.   SynEdit1.Options := opts;
  1031. end;
  1032.  
  1033. procedure TForm1.FileModTimerTimer(Sender: TObject);
  1034. resourcestring
  1035.   SChangeConflict = 'The file was changed in a different application BUT IT WAS ALSO MODIFIED HERE! Reload file AND LOSE CHANGES HERE?';
  1036.   SChangeNotify = 'The file was changed in a different application! Reload file?';
  1037. begin
  1038.   FileModTimer.Enabled := false;
  1039.   if FileModLast <> FileAge(GetScrapFile) then
  1040.   begin
  1041.     FileModLast := FileAge(GetScrapFile);
  1042.     if SynEdit1.Modified then
  1043.     begin
  1044.       if MessageDlg(SChangeConflict, mtWarning, mbYesNoCancel, 0) = mrYes then
  1045.       begin
  1046.         SynEdit1.Lines.LoadFromFile(GetScrapFile);
  1047.       end;
  1048.     end
  1049.     else
  1050.     begin
  1051.       if MessageDlg(SChangeNotify, mtConfirmation, mbYesNoCancel, 0) = mrYes then
  1052.       begin
  1053.         SynEdit1.Lines.LoadFromFile(GetScrapFile);
  1054.       end;
  1055.     end;
  1056.   end;
  1057.   FileModTimer.Enabled := true;
  1058. end;
  1059.  
  1060. procedure TForm1.FontSizeTimerTimer(Sender: TObject);
  1061. begin
  1062.   if Memo2.Font.Size <> SynEdit1.Font.Size then
  1063.     Memo2.Font.Size := SynEdit1.Font.Size;
  1064. end;
  1065.  
  1066. procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
  1067. begin
  1068.   DragAcceptFiles(Handle, False);
  1069.   TFastPHPConfig.FontSize := SynEdit1.Font.Size;
  1070. end;
  1071.  
  1072. procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  1073. var
  1074.   r: integer;
  1075. resourcestring
  1076.   SWantToSave = 'Do you want to save?';
  1077. begin
  1078.   if SynEdit1.Modified then
  1079.   begin
  1080.     if (ParamStr(1) <> '') or (FSaveAsFilename <> '') then
  1081.     begin
  1082.       r := MessageDlg(SWantToSave, mtConfirmation, mbYesNoCancel, 0);
  1083.       if r = mrCancel then
  1084.       begin
  1085.         CanClose := false;
  1086.         Exit;
  1087.       end
  1088.       else if r = mrYes then
  1089.       begin
  1090.         ActionSave.Execute;
  1091.         CanClose := true;
  1092.       end;
  1093.     end
  1094.     else
  1095.     begin
  1096.       ActionSave.Execute;
  1097.       CanClose := true;
  1098.     end;
  1099.   end;
  1100. end;
  1101.  
  1102. procedure TForm1.FormCreate(Sender: TObject);
  1103. var
  1104.   exeDir: string;
  1105.   sScrapFile: string;
  1106. begin
  1107.   HlpPrevPageIndex := -1;
  1108.   CurSearchTerm := '';
  1109.   sScrapFile := GetScrapFile;
  1110.   Caption := Caption + ' - ' + sScrapFile;
  1111.   Application.Title := Format('%s - FastPHP', [ExtractFileName(sScrapFile)]); // do not translate!
  1112.   SrcRep := TSynEditFindReplace.Create(self);
  1113.   SrcRep.Editor := SynEdit1;
  1114.   SynEdit1.Gutter.Gradient := HighColorWindows;
  1115.  
  1116.   Screen.Cursors[crMouseGutter] := LoadCursor(hInstance, 'MOUSEGUTTER');
  1117.   SynEdit1.Gutter.Cursor := crMouseGutter;
  1118.  
  1119.   exeDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
  1120.   if FileExists(exeDir + 'codeexplorer.bmp') then ImageList1.LoadAndSplitImages(exeDir + 'codeexplorer.bmp');
  1121.  
  1122.   FileModLast := FileAge(sScrapFile);
  1123.   FileModTimer.Enabled := True;
  1124. end;
  1125.  
  1126. procedure TForm1.FormDestroy(Sender: TObject);
  1127. begin
  1128.   if Assigned(ChmIndex) then
  1129.   begin
  1130.     FreeAndNil(ChmIndex);
  1131.   end;
  1132.   FreeAndNil(SrcRep);
  1133.  
  1134.   if hMutex <> 0 then CloseHandle(hMutex); // Note: ReleaseMutex does not work as expected!
  1135.  
  1136.   if Assigned(codeExplorer) then
  1137.   begin
  1138.     codeExplorer.Terminate;
  1139.     codeExplorer.WaitFor;
  1140.     FreeAndNil(codeExplorer);
  1141.   end;
  1142. end;
  1143.  
  1144. procedure TForm1.FormShow(Sender: TObject);
  1145. var
  1146.   ScrapFile: string;
  1147.   tmpFontSize: integer;
  1148.   opts: TSynEditorOptions;
  1149. resourcestring
  1150.   SFileAlreadyOpen = 'File "%s" is alrady open!';
  1151. begin
  1152.   if FormShowRanOnce then exit; // If the theme is changed from normal to dark, OnShow will be called another time
  1153.   FormShowRanOnce := true;
  1154.  
  1155.   ScrapFile := GetScrapFile;
  1156.   if ScrapFile = '' then
  1157.   begin
  1158.     Application.Terminate; // Close;
  1159.     exit;
  1160.   end;
  1161.  
  1162.   opts := SynEdit1.Options;
  1163.   if TFastPHPConfig.SpecialChars then
  1164.   begin
  1165.     BtnSpecialChars.Picture.Assign(BtnSpecialCharsOn.Picture);
  1166.     Include(opts, eoShowSpecialChars);
  1167.   end
  1168.   else
  1169.   begin
  1170.     BtnSpecialChars.Picture.Assign(BtnSpecialCharsOff.Picture);
  1171.     Exclude(opts, eoShowSpecialChars);
  1172.   end;
  1173.   SynEdit1.Options := opts;
  1174.  
  1175.   if FileExists(ScrapFile) then
  1176.   begin
  1177.     if hMutex = 0 then
  1178.     begin
  1179.       hMutex := CreateMutex(nil, True, PChar('FastPHP'+md5(UpperCase(ScrapFile))));
  1180.       if GetLastError = ERROR_ALREADY_EXISTS then
  1181.       begin
  1182.         // TODO: It would be great if the window of that FastPHP instance would switched to foreground
  1183.         ShowMessageFmt(SFileAlreadyOpen, [ScrapFile]);
  1184.         Close;
  1185.       end;
  1186.  
  1187.       SynEdit1.Lines.LoadFromFile(ScrapFile);
  1188.     end;
  1189.   end
  1190.   else
  1191.     SynEdit1.Lines.Clear;
  1192.  
  1193.   PageControl1.ActivePage := PlaintextTabSheet;
  1194.  
  1195.   PageControl2.ActivePage := CodeTabsheet;
  1196.   HelpTabsheet.TabVisible := false;
  1197.  
  1198.   tmpFontSize := TFastPHPConfig.FontSize;
  1199.   if tmpFontSize <> -1 then SynEdit1.Font.Size := tmpFontSize;
  1200.   SynEdit1.SetFocus;
  1201.  
  1202.   DoubleBuffered := true;
  1203.   StartCodeExplorer;
  1204.  
  1205.   DragAcceptFiles(Handle, True);
  1206.  
  1207.   StartupTimer.Enabled := true;
  1208. end;
  1209.  
  1210. function TForm1.ContainsUnicodeCharsInAnsiFile: boolean;
  1211. var
  1212.   lines: TStringList;
  1213.   ms: TMemoryStream;
  1214. begin
  1215.   if SynEdit1.Lines.Encoding = TEncoding.UTF8 then
  1216.   begin
  1217.     result := false;
  1218.     exit;
  1219.   end;
  1220.  
  1221.   lines := TStringList.Create;
  1222.   ms := TMemoryStream.Create;
  1223.   try
  1224.     SynEdit1.Lines.SaveToStream(ms);
  1225.     ms.Position := 0;
  1226.     lines.LoadFromStream(ms);
  1227.     result := Trim(lines.Text) <> Trim(synedit1.Lines.Text);
  1228.   finally
  1229.     FreeAndNil(ms);
  1230.     FreeAndNil(lines);
  1231.   end;
  1232. end;
  1233.  
  1234. procedure TForm1.SaveToFile(filename: string);
  1235. var
  1236.   ss: TStringStream;
  1237.   ms: TMemoryStream;
  1238.   fs: TFileStream;
  1239.   eolStyle: string;
  1240.   str: string;
  1241.   UpgradeEncodingToUnicode: boolean;
  1242.   DoReloadEncoding: boolean;
  1243. resourcestring
  1244.   LNG_UpgradeToUtf8 = 'Unicode characters have been inserted to this ANSI encoded file. Convert file to UTF-8? (If you choose "No", the Unicode characters will be replaced with "?")';
  1245. begin
  1246.   FileModTimer.Enabled := false;
  1247.  
  1248.   if ContainsUnicodeCharsInAnsiFile then
  1249.   begin
  1250.     DoReloadEncoding := true;
  1251.     case MessageBox(Handle, PChar(LNG_UpgradeToUtf8), PChar(Caption), MB_YESNOCANCEL) of
  1252.       ID_YES:
  1253.       begin
  1254.         UpgradeEncodingToUnicode := true;
  1255.       end;
  1256.       ID_NO:
  1257.       begin
  1258.         UpgradeEncodingToUnicode := false;
  1259.       end;
  1260.       ID_CANCEL:
  1261.       begin
  1262.         Abort;
  1263.       end;
  1264.     end;
  1265.   end
  1266.   else
  1267.   begin
  1268.     DoReloadEncoding := false;
  1269.     UpgradeEncodingToUnicode := false;
  1270.   end;
  1271.  
  1272.   ms := TMemoryStream.Create;
  1273.   ss := TStringStream.Create('');
  1274.   fs := TFileStream.Create(filename, fmCreate);
  1275.   try
  1276.     // Save everything in a memory stream and then to a string
  1277.     // in comparison to "str := SynEdit1.Lines.Text;",
  1278.     // This approach should preserve LF / CRLF line endings
  1279.     if UpgradeEncodingToUnicode then
  1280.       SynEdit1.Lines.SaveToStream(ms, TEncoding.UTF8) // upgrade an ANSI file to UTF-8 (e.g. if you include Japanese characters)
  1281.     else
  1282.       SynEdit1.Lines.SaveToStream(ms);
  1283.     if DoReloadEncoding then
  1284.     begin
  1285.       ms.Position := 0;
  1286.       SynEdit1.Lines.LoadFromStream(ms);
  1287.     end;
  1288.     ms.Position := 0;
  1289.     ss.CopyFrom(ms, ms.Size);
  1290.     ss.Position := 0;
  1291.     str := ss.ReadString(ss.Size);
  1292.     ss.Size := 0; // clear string-stream, because we need it later again
  1293.  
  1294.     // Detect current line-endings
  1295.     if Copy(str, 1, 2) = '#!' then
  1296.     begin
  1297.       // Shebang. Use ONLY Linux LF
  1298.       str := StringReplace(str, #13#10, #10, [rfReplaceAll]);
  1299.       eolStyle := #10 // Linux LF
  1300.     end
  1301.     else
  1302.     begin
  1303.       if Pos(#13#10, str) > 0 then
  1304.         eolStyle := #13#10 // Windows CRLF
  1305.       else if Pos(#10, str) > 0 then
  1306.         eolStyle := #10 // Linux LF
  1307.       else
  1308.       begin
  1309.         if DefaultTextLineBreakStyle = tlbsLF then
  1310.           eolStyle := #10 // Linux LF
  1311.         else if DefaultTextLineBreakStyle = tlbsCRLF then
  1312.           eolStyle := #13#10 // Windows CRLF
  1313.         //else if DefaultTextLineBreakStyle = tlbsCR then
  1314.         //  eolStyle := #13 // Old Mac CR
  1315.         else
  1316.           eolStyle := #13#10; // (Should not happen)
  1317.       end;
  1318.     end;
  1319.  
  1320.     // Unitfy line-endings
  1321.     str := StringReplace(str, #13, '', [rfReplaceAll]);
  1322.     str := StringReplace(str, #10, eolStyle, [rfReplaceAll]);
  1323.  
  1324.     // Replace all trailing linebreaks by a single line break
  1325.     // Note: Removing all line breaks is not good, since Linux's "nano" will
  1326.     //       re-add a linebreak at the end of the file
  1327.     str := TrimRight(str) + eolStyle;
  1328.  
  1329.     // Old versions of Delphi/SynEdit write an UTF-8 BOM, which makes problems
  1330.     // e.g. with AJAX handlers (because AJAX reponses must not have a BOM).
  1331.     // So we try to avoid that.
  1332.     // Note that the output is still UTF-8 encoded if the input file was UTF-8 encoded
  1333.     if Copy(str,1,3) = #$EF#$BB#$BF then Delete(str, 1, 3);
  1334.  
  1335.     // Now save to the file
  1336.     ss.WriteString(str);
  1337.     ss.Position := 0;
  1338.     fs.CopyFrom(ss, ss.Size-ss.Position);
  1339.   finally
  1340.     FreeAndNil(ms);
  1341.     FreeAndNil(ss);
  1342.     FreeAndNil(fs);
  1343.   end;
  1344.  
  1345.   FileModLast := FileAge(GetScrapFile);
  1346.   FileModTimer.Enabled := True;
  1347. end;
  1348.  
  1349. procedure TForm1.StartCodeExplorer;
  1350. begin
  1351.   codeExplorer := TRunCodeExplorer.Create(true);
  1352.   codeExplorer.InputRequestCallback := InputRequestCallback;
  1353.   codeExplorer.OutputNotifyCallback := OutputNotifyCallback;
  1354.   codeExplorer.PhpExe := GetPHPExe;
  1355.   codeExplorer.PhpFile := IncludeTrailingPathDelimiter(ExtractFileDir(Application.ExeName)) + 'codeexplorer.php'; // GetScrapFile;
  1356.   codeExplorer.WorkDir := ExtractFileDir(Application.ExeName);
  1357.   codeExplorer.Resume;
  1358. end;
  1359.  
  1360. procedure TForm1.StartUpTimerTimer(Sender: TObject);
  1361. begin
  1362.   StartupTimer.Enabled := false;
  1363.  
  1364.   // We need this timer because we cannot change the Theme during OnShow,
  1365.   // because the Delphi VCL Theme is buggy!
  1366.  
  1367.   if TFastPHPConfig.DarkTheme then
  1368.   begin
  1369.     BtnLight.Picture.Assign(BtnLightOff.Picture);
  1370.     Theme_Dark;
  1371.   end
  1372.   else
  1373.   begin
  1374.     BtnLight.Picture.Assign(BtnLightOn.Picture);
  1375.     Theme_Light;
  1376.   end;
  1377. end;
  1378.  
  1379. function TForm1.GetScrapFile: string;
  1380. var
  1381.   tmpPath: string;
  1382. resourcestring
  1383.   SFileDoesNotExistsCreate = 'File %s does not exist. Create it?';
  1384.   SPathDoesNotExistTryAgain = 'Path does not exist! Please try again.';
  1385. begin
  1386.   if FSaveAsFilename <> '' then
  1387.   begin
  1388.     result := FSaveAsFilename;
  1389.     exit;
  1390.   end;
  1391.  
  1392.   if FScrapFile <> '' then
  1393.   begin
  1394.     result := FScrapFile;
  1395.     exit;
  1396.   end;
  1397.  
  1398.   if ParamStr(1) <> '' then
  1399.   begin
  1400.     // Program was started with a filename
  1401.  
  1402.     result := ParamStr(1);
  1403.  
  1404.     if not FileExists(result) then
  1405.     begin
  1406.       case MessageDlg(Format(SFileDoesNotExistsCreate, [result]), mtConfirmation, mbYesNoCancel, 0) of
  1407.         mrYes:
  1408.           try
  1409.             SaveToFile(result);
  1410.           except
  1411.             on E: Exception do
  1412.             begin
  1413.               MessageDlg(E.Message, mtError, [mbOk], 0);
  1414.               Application.Terminate;
  1415.               result := '';
  1416.               exit;
  1417.             end;
  1418.           end;
  1419.         mrNo:
  1420.           begin
  1421.             Application.Terminate;
  1422.             result := '';
  1423.             exit;
  1424.           end;
  1425.         mrCancel:
  1426.           begin
  1427.             Application.Terminate;
  1428.             result := '';
  1429.             exit;
  1430.           end;
  1431.       end;
  1432.     end;
  1433.   end
  1434.   else
  1435.   begin
  1436.     // Program is started without filename -> use scrap file
  1437.  
  1438.     result := TFastPHPConfig.ScrapFile;
  1439.  
  1440.     if not FileExists(result) then
  1441.     begin
  1442.       repeat
  1443.         {$REGION 'Determinate opendialog initial directory'}
  1444.         if result <> '' then
  1445.         begin
  1446.           tmpPath := ExtractFilePath(result);
  1447.           if DirectoryExists(tmpPath) then
  1448.           begin
  1449.             OpenDialog3.InitialDir := tmpPath;
  1450.             OpenDialog3.FileName := Result;
  1451.           end
  1452.           else
  1453.           begin
  1454.             OpenDialog3.InitialDir := GetMyDocumentsFolder;
  1455.           end;
  1456.         end
  1457.         else
  1458.         begin
  1459.           OpenDialog3.InitialDir := GetMyDocumentsFolder;
  1460.         end;
  1461.         {$ENDREGION}
  1462.  
  1463.         if not OpenDialog3.Execute then
  1464.         begin
  1465.           Application.Terminate;
  1466.           result := '';
  1467.           exit;
  1468.         end;
  1469.  
  1470.         if not DirectoryExists(ExtractFilePath(OpenDialog3.FileName)) then
  1471.         begin
  1472.           MessageDlg(SPathDoesNotExistTryAgain, mtWarning, [mbOk], 0);
  1473.         end
  1474.         else
  1475.         begin
  1476.           result := OpenDialog3.FileName;
  1477.         end;
  1478.       until result <> '';
  1479.  
  1480.       if not FileExists(result) then
  1481.       begin
  1482.         try
  1483.           // Try saving the file; check if we have permissions
  1484.           //SynEdit1.Lines.Clear;
  1485.           SaveToFile(result);
  1486.         except
  1487.           on E: Exception do
  1488.           begin
  1489.             MessageDlg(E.Message, mtError, [mbOk], 0);
  1490.             Application.Terminate;
  1491.             result := '';
  1492.             exit;
  1493.           end;
  1494.         end;
  1495.       end;
  1496.  
  1497.       TFastPHPConfig.ScrapFile := result;
  1498.       FScrapFile := result;
  1499.     end;
  1500.   end;
  1501. end;
  1502.  
  1503. procedure TForm1.Help;
  1504. var
  1505.   IndexFile, chmFile, w, OriginalWord, url: string;
  1506.   internalHtmlFile: string;
  1507. resourcestring
  1508.   SChmFileNotAValidPHPDocumentation = 'The CHM file is not a valid PHP documentation. Cannot use help.';
  1509.   SUnknownErrorCannotUseHelp = 'Unknown error. Cannot use help.';
  1510.   SNoHelpAvailable = 'No help for "%s" available';
  1511. begin
  1512.   if not Assigned(ChmIndex) then
  1513.   begin
  1514.     IndexFile := TFastPHPConfig.HelpIndex;
  1515.     IndexFile := ChangeFileExt(IndexFile, '.ini'); // Just to be sure. Maybe someone wrote manually the ".chm" file in there
  1516.     if FileExists(IndexFile) then
  1517.     begin
  1518.       ChmIndex := TMemIniFile.Create(IndexFile);
  1519.     end;
  1520.   end;
  1521.  
  1522.   if Assigned(ChmIndex) then
  1523.   begin
  1524.     IndexFile := TFastPHPConfig.HelpIndex;
  1525.     // We don't check if IndexFile still exists. It is not important since we have ChmIndex pre-loaded in memory
  1526.  
  1527.     chmFile := ChangeFileExt(IndexFile, '.chm');
  1528.     if not FileExists(chmFile) then
  1529.     begin
  1530.       FreeAndNil(ChmIndex);
  1531.     end;
  1532.   end;
  1533.  
  1534.   if not Assigned(ChmIndex) then
  1535.   begin
  1536.     if not OpenDialog1.Execute then exit;
  1537.  
  1538.     chmFile := OpenDialog1.FileName;
  1539.     if not FileExists(chmFile) then exit;
  1540.  
  1541.     IndexFile := ChangeFileExt(chmFile, '.ini');
  1542.  
  1543.     if not FileExists(IndexFile) then
  1544.     begin
  1545.       Panel1.Align := alClient;
  1546.       Panel1.Visible := true;
  1547.       Panel1.BringToFront;
  1548.       Screen.Cursor := crHourGlass;
  1549.       Application.ProcessMessages;
  1550.       try
  1551.         if not ParseCHM(chmFile) then
  1552.         begin
  1553.           MessageDlg(SChmFileNotAValidPHPDocumentation, mtError, [mbOk], 0);
  1554.           exit;
  1555.         end;
  1556.       finally
  1557.         Screen.Cursor := crDefault;
  1558.         Panel1.Visible := false;
  1559.       end;
  1560.  
  1561.       if not FileExists(IndexFile) then
  1562.       begin
  1563.         MessageDlg(SUnknownErrorCannotUseHelp, mtError, [mbOk], 0);
  1564.         exit;
  1565.       end;
  1566.     end;
  1567.  
  1568.     TFastPHPConfig.HelpIndex := IndexFile;
  1569.  
  1570.     ChmIndex := TMemIniFile.Create(IndexFile);
  1571.   end;
  1572.  
  1573.   w := GetWordUnderCaret(SynEdit1);
  1574.   if w = '' then exit;
  1575.   {$IFDEF UNICODE}
  1576.   if CharInSet(w[1], ['0'..'9']) then exit;
  1577.   {$ELSE}
  1578.   if w[1] in ['0'..'9'] then exit;
  1579.   {$ENDIF}
  1580.  
  1581.   Originalword := w;
  1582. //  w := StringReplace(w, '_', '-', [rfReplaceAll]);
  1583.   w := LowerCase(w);
  1584.   CurSearchTerm := w;
  1585.  
  1586.   internalHtmlFile := ChmIndex.ReadString('function', CurSearchTerm, ''); // do not translate
  1587.   if internalHtmlFile = '' then
  1588.     internalHtmlFile := ChmIndex.ReadString('_HelpWords_', CurSearchTerm, ''); // do not translate
  1589.   if internalHtmlFile = '' then
  1590.   begin
  1591.     HelpTabsheet.TabVisible := false;
  1592.     HlpPrevPageIndex := -1;
  1593.     ShowMessageFmt(SNoHelpAvailable, [Originalword]);
  1594.     Exit;
  1595.   end;
  1596.  
  1597.   url := 'mk:@MSITStore:'+ChmFile+'::'+internalHtmlFile;
  1598.  
  1599.   HlpPrevPageIndex := PageControl2.ActivePageIndex; // Return by pressing ESC
  1600.   HelpTabsheet.TabVisible := true;
  1601.   PageControl2.ActivePage := HelpTabsheet;
  1602.   WebBrowser2.Navigate(url);
  1603.   WebBrowser2.Wait;
  1604. end;
  1605.  
  1606. procedure TForm1.GotoLineNo(LineNo:integer);
  1607. var
  1608.   line: string;
  1609.   i: integer;
  1610. begin
  1611.   SynEdit1.GotoLineAndCenter(LineNo);
  1612.  
  1613.   // Skip indent
  1614.   line := SynEdit1.Lines[SynEdit1.CaretY];
  1615.   for i := 1 to Length(line) do
  1616.   begin
  1617.     {$IFDEF UNICODE}
  1618.     if not CharInSet(line[i], [' ', #9]) then
  1619.     {$ELSE}
  1620.     if not (line[i] in [' ', #9]) then
  1621.     {$ENDIF}
  1622.     begin
  1623.       SynEdit1.CaretX := i-1;
  1624.       break;
  1625.     end;
  1626.   end;
  1627.  
  1628.   PageControl2.ActivePage := CodeTabsheet;
  1629.   if SynEdit1.CanFocus then SynEdit1.SetFocus;
  1630. end;
  1631.  
  1632. procedure TForm1.PageControl2Changing(Sender: TObject;
  1633.   var AllowChange: Boolean);
  1634. begin
  1635.   if PageControl2.ActivePage = HelpTabsheet then
  1636.     HlpPrevPageIndex := -1
  1637.   else
  1638.     HlpPrevPageIndex := PageControl2.ActivePageIndex;
  1639.  
  1640.   AllowChange := true;
  1641. end;
  1642.  
  1643. procedure TForm1.Memo2DblClick(Sender: TObject);
  1644. var
  1645.   line: string;
  1646.  
  1647.   procedure _process(toFind: string);
  1648.   var
  1649.     p, lineno: integer;
  1650.   begin
  1651.     if FileSystemCaseSensitive then
  1652.       p := Pos(toFind, line)
  1653.     else
  1654.       p := Pos(LowerCase(toFind), LowerCase(line));
  1655.     if p <> 0 then
  1656.     begin
  1657.       line := copy(line, p+length(toFind), 99);
  1658.       if not TryStrToInt(line, lineno) then exit;
  1659.       GotoLineNo(lineno);
  1660.     end;
  1661.   end;
  1662.  
  1663. begin
  1664.   line := memo2.Lines.Strings[Memo2.CaretPos.Y];
  1665.  
  1666.   {$REGION 'Possibility 1: filename.php:lineno'}
  1667.   _process(ExtractFileName(GetScrapFile) + ':');
  1668.   {$ENDREGION}
  1669.  
  1670.   {$REGION 'Possibility 2: on line xx'}
  1671.   _process(ExtractFileName(GetScrapFile) + ' on line '); // do not translate!
  1672.   {$ENDREGION}
  1673. end;
  1674.  
  1675. procedure TForm1.Memo2KeyDown(Sender: TObject; var Key: Word;
  1676.   Shift: TShiftState);
  1677. begin
  1678.   if ((ssCtrl in Shift) and (Key = 65)) then TMemo(Sender).SelectAll;
  1679. end;
  1680.  
  1681. function TForm1.MarkUpLineReference(cont: string): string;
  1682.  
  1683.   procedure _process(toFind: string);
  1684.   var
  1685.     p, a, b: integer;
  1686.     num: integer;
  1687.     insert_a, insert_b: string;
  1688.   begin
  1689.     if FileSystemCaseSensitive then
  1690.       p := Pos(toFind, cont)
  1691.     else
  1692.       p := Pos(LowerCase(toFind), LowerCase(cont));
  1693.     while p >= 1 do
  1694.     begin
  1695.       a := p;
  1696.       b := p + length(toFind);
  1697.       num := 0;
  1698.       {$IFDEF UNICODE}
  1699.       while CharInSet(cont[b], ['0'..'9']) do
  1700.       {$ELSE}
  1701.       while cont[b] in ['0'..'9'] do
  1702.       {$ENDIF}
  1703.       begin
  1704.         num := num*10 + StrToInt(cont[b]);
  1705.         inc(b);
  1706.       end;
  1707.  
  1708.       insert_b := '</a>';
  1709.       insert_a := '<a href="' + FASTPHP_GOTO_URI_PREFIX + IntToStr(num) + '">';
  1710.  
  1711.       insert(insert_b, cont, b);
  1712.       insert(insert_a, cont, a);
  1713.  
  1714.       p := b + Length(insert_a) + Length(insert_b);
  1715.  
  1716.       p := PosEx(toFind, cont, p+1);
  1717.     end;
  1718.   end;
  1719.  
  1720. begin
  1721.   {$REGION 'Possibility 1: filename.php:lineno'}
  1722.   _process(ExtractFileName(GetScrapFile) + ':');
  1723.   {$ENDREGION}
  1724.  
  1725.   {$REGION 'Possibility 2: on line xx'}
  1726.   _process(ExtractFileName(GetScrapFile) + ' on line '); // do not translate!
  1727.   {$ENDREGION}
  1728.  
  1729.   result := cont;
  1730. end;
  1731.  
  1732. function TForm1.InputRequestCallback(var data: AnsiString): boolean;
  1733. begin
  1734.   data := UTF8Encode(SynEdit1.Text);
  1735.   result := true;
  1736. end;
  1737.  
  1738. function TForm1.IsThemeDark: boolean;
  1739. begin
  1740.   result := Assigned(TStyleManager.ActiveStyle) and (TStyleManager.ActiveStyle.Name<>'Windows');
  1741. end;
  1742.  
  1743. function TForm1.OutputNotifyCallback(const data: AnsiString): boolean;
  1744. begin
  1745.   result := TreeView1.FillWithFastPHPData(string(data));
  1746. end;
  1747.  
  1748. end.
  1749.