Subversion Repositories fastphp

Rev

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