IDE.MainForm.AutoCompleteAndCallTipsHelper.pas 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. unit IDE.MainForm.AutoCompleteAndCallTipsHelper;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. Compiler form - Auto complete & call tips helper which has the tools helper as ancestor
  8. Not used by MainForm: it uses IDE.MainForm.FinalHelper instead
  9. }
  10. interface
  11. uses
  12. Menus,
  13. ScintEdit,
  14. IDE.MainForm, IDE.MainForm.ToolsHelper;
  15. type
  16. TMainFormAutoCompleteAndCallTipsHelper = class helper(TMainFormToolsHelper) for TMainForm
  17. procedure InitiateAutoComplete(const AMemo: TScintEdit; const Key: AnsiChar);
  18. procedure AutoCompleteAndCallTipsHandleCharAdded(const AMemo: TScintEdit; const Ch: AnsiChar);
  19. procedure CallTipsHandleArrowClick(const AMemo: TScintEdit; const Up: Boolean);
  20. procedure CallTipsHandleCtrlSpace(const AMemo: TScintEdit);
  21. { Private }
  22. function _InitiateAutoCompleteOrCallTipAllowedAtPos(const AMemo: TScintEdit;
  23. const WordStartLinePos, PositionBeforeWordStartPos: Integer): Boolean;
  24. procedure _UpdateCallTipFunctionDefinition(const AMemo: TScintEdit; const Pos: Integer = -1);
  25. procedure _InitiateCallTip(const AMemo: TScintEdit; const Key: AnsiChar);
  26. procedure _ContinueCallTip(const AMemo: TScintEdit);
  27. end;
  28. implementation
  29. uses
  30. SysUtils, Math,
  31. IDE.ScintStylerInnoSetup;
  32. function TMainFormAutoCompleteAndCallTipsHelper._InitiateAutoCompleteOrCallTipAllowedAtPos(const AMemo: TScintEdit;
  33. const WordStartLinePos, PositionBeforeWordStartPos: Integer): Boolean;
  34. begin
  35. Result := (PositionBeforeWordStartPos < WordStartLinePos) or
  36. not FMemosStyler.IsCommentOrPascalStringStyle(AMemo.GetStyleAtPosition(PositionBeforeWordStartPos));
  37. end;
  38. procedure TMainFormAutoCompleteAndCallTipsHelper.InitiateAutoComplete(const AMemo: TScintEdit; const Key: AnsiChar);
  39. function OnlyWhiteSpaceBeforeWord(const AMemo: TScintEdit; const LinePos, WordStartPos: Integer): Boolean;
  40. var
  41. I: Integer;
  42. C: AnsiChar;
  43. begin
  44. { Only allow autocompletion if no non-whitespace characters exist before the current word on the line }
  45. I := WordStartPos;
  46. Result := False;
  47. while I > LinePos do begin
  48. I := AMemo.GetPositionBefore(I);
  49. if I < LinePos then
  50. Exit; { shouldn't get here }
  51. C := AMemo.GetByteAtPosition(I);
  52. if C > ' ' then
  53. Exit;
  54. end;
  55. Result := True;
  56. end;
  57. var
  58. CaretPos, Line, LinePos, WordStartPos, WordEndPos, CharsBefore,
  59. LangNamePos: Integer;
  60. Section: TInnoSetupStylerSection;
  61. IsParamSection: Boolean;
  62. WordList: AnsiString;
  63. FoundSemicolon, FoundFlagsOrType, FoundDot: Boolean;
  64. C: AnsiChar;
  65. begin
  66. if AMemo.AutoCompleteActive or AMemo.ReadOnly then
  67. Exit;
  68. if Key = #0 then begin
  69. { If a character is typed then Scintilla will handle selections but
  70. otherwise we should empty them and also make sure the caret is visible
  71. before we start autocompletion }
  72. AMemo.SetEmptySelections;
  73. AMemo.ScrollCaretIntoView;
  74. end;
  75. CaretPos := AMemo.CaretPosition;
  76. Line := AMemo.GetLineFromPosition(CaretPos);
  77. LinePos := AMemo.GetPositionFromLine(Line);
  78. WordStartPos := AMemo.GetWordStartPosition(CaretPos, True);
  79. WordEndPos := AMemo.GetWordEndPosition(CaretPos, True);
  80. CharsBefore := CaretPos - WordStartPos;
  81. { Don't auto start autocompletion after a character is typed if there are any
  82. word characters adjacent to the character }
  83. if Key <> #0 then begin
  84. if CharsBefore > 1 then
  85. Exit;
  86. if WordEndPos > CaretPos then
  87. Exit;
  88. end;
  89. case AMemo.GetByteAtPosition(WordStartPos) of
  90. '#':
  91. begin
  92. if not OnlyWhiteSpaceBeforeWord(AMemo, LinePos, WordStartPos) then
  93. Exit;
  94. WordList := FMemosStyler.ISPPDirectivesWordList;
  95. AMemo.SetAutoCompleteFillupChars(' ');
  96. end;
  97. '{':
  98. begin
  99. WordList := FMemosStyler.ConstantsWordList;
  100. AMemo.SetAutoCompleteFillupChars('\:');
  101. end;
  102. '[':
  103. begin
  104. if not OnlyWhiteSpaceBeforeWord(AMemo, LinePos, WordStartPos) then
  105. Exit;
  106. WordList := FMemosStyler.SectionsWordList;
  107. AMemo.SetAutoCompleteFillupChars('');
  108. end;
  109. else
  110. begin
  111. Section := FMemosStyler.GetSectionFromLineState(AMemo.Lines.State[Line]);
  112. if Section = scCode then begin
  113. { Space can only initiate autocompletion after non whitespace }
  114. if (Key = ' ') and OnlyWhiteSpaceBeforeWord(AMemo, LinePos, WordStartPos) then
  115. Exit;
  116. var PositionBeforeWordStartPos := AMemo.GetPositionBefore(WordStartPos);
  117. if Key <> #0 then begin
  118. AMemo.StyleNeeded(PositionBeforeWordStartPos); { Make sure the typed character has been styled }
  119. if not _InitiateAutoCompleteOrCallTipAllowedAtPos(AMemo, LinePos, PositionBeforeWordStartPos) then
  120. Exit;
  121. end;
  122. WordList := '';
  123. { Autocomplete event functions if the current word on the line has
  124. exactly 1 space before it which has the word 'function' or
  125. 'procedure' before it which has only whitespace before it }
  126. if (PositionBeforeWordStartPos >= LinePos) and (AMemo.GetByteAtPosition(PositionBeforeWordStartPos) <= ' ') then begin
  127. var FunctionWordEndPos := PositionBeforeWordStartPos;
  128. var FunctionWordStartPos := AMemo.GetWordStartPosition(FunctionWordEndPos, True);
  129. if OnlyWhiteSpaceBeforeWord(AMemo, LinePos, FunctionWordStartPos) then begin
  130. var FunctionWord := AMemo.GetTextRange(FunctionWordStartPos, FunctionWordEndPos);
  131. if SameText(FunctionWord, 'procedure') then
  132. WordList := FMemosStyler.EventFunctionsWordList[True]
  133. else if SameText(FunctionWord, 'function') then
  134. WordList := FMemosStyler.EventFunctionsWordList[False];
  135. if WordList <> '' then
  136. AMemo.SetAutoCompleteFillupChars('');
  137. end;
  138. end;
  139. { If no event function was found then autocomplete script functions,
  140. types, etc if the current word has no dot before it }
  141. if WordList = '' then begin
  142. var ClassOrRecordMember := (PositionBeforeWordStartPos >= LinePos) and (AMemo.GetByteAtPosition(PositionBeforeWordStartPos) = '.');
  143. WordList := FMemosStyler.ScriptWordList[ClassOrRecordMember];
  144. AMemo.SetAutoCompleteFillupChars('');
  145. end;
  146. if WordList = '' then
  147. Exit;
  148. end else begin
  149. IsParamSection := FMemosStyler.IsParamSection(Section);
  150. { Autocomplete if the current word on the line has only whitespace
  151. before it, or else also: after the last ';' or after 'Flags:' or
  152. 'Type:' in parameterized sections }
  153. FoundSemicolon := False;
  154. FoundFlagsOrType := False;
  155. FoundDot := False;
  156. var I := WordStartPos;
  157. while I > LinePos do begin
  158. I := AMemo.GetPositionBefore(I);
  159. if I < LinePos then
  160. Exit; { shouldn't get here }
  161. C := AMemo.GetByteAtPosition(I);
  162. if IsParamSection and (C in [';', ':']) and
  163. FMemosStyler.IsSymbolStyle(AMemo.GetStyleAtPosition(I)) then begin { Make sure it's an stSymbol ';' or ':' and not one inside a quoted string }
  164. FoundSemicolon := C = ';';
  165. if not FoundSemicolon then begin
  166. var ParameterWordEndPos := I;
  167. var ParameterWordStartPos := AMemo.GetWordStartPosition(ParameterWordEndPos, True);
  168. var ParameterWord := AMemo.GetTextRange(ParameterWordStartPos, ParameterWordEndPos);
  169. FoundFlagsOrType := SameText(ParameterWord, 'Flags') or
  170. ((Section in [scInstallDelete, scUninstallDelete]) and SameText(ParameterWord, 'Type'));
  171. end else
  172. FoundFlagsOrType := False;
  173. if FoundSemicolon or FoundFlagsOrType then
  174. Break;
  175. end;
  176. if (Section = scLangOptions) and (C = '.') and not FoundDot then begin
  177. { Verify that a word (language name) precedes the '.', then check for
  178. any non-whitespace characters before the word }
  179. LangNamePos := AMemo.GetWordStartPosition(I, True);
  180. if LangNamePos >= I then
  181. Exit;
  182. I := LangNamePos;
  183. FoundDot := True;
  184. end else if C > ' ' then begin
  185. if IsParamSection and not (Section in [scInstallDelete, scUninstallDelete]) and
  186. (FMemosStyler.FlagsWordList[Section] <> '') then begin
  187. { Verify word before the current word (or before that when we get here again) is
  188. a valid flag and if so, continue looking before it instead of stopping }
  189. var FlagEndPos := AMemo.GetWordEndPosition(I, True);
  190. var FlagStartPos := AMemo.GetWordStartPosition(I, True);
  191. var FlagWord := AMemo.GetTextRange(FlagStartPos, FlagEndPos);
  192. if FMemosStyler.SectionHasFlag(Section, FlagWord) then
  193. I := FlagStartPos
  194. else
  195. Exit;
  196. end else
  197. Exit;
  198. end;
  199. end;
  200. { Space can only initiate autocompletion after ';' or 'Flags:' or 'Type:' in parameterized sections }
  201. if (Key = ' ') and not (FoundSemicolon or FoundFlagsOrType) then
  202. Exit;
  203. if FoundFlagsOrType then begin
  204. WordList := FMemosStyler.FlagsWordList[Section];
  205. if WordList = '' then
  206. Exit;
  207. AMemo.SetAutoCompleteFillupChars(' ');
  208. end else begin
  209. WordList := FMemosStyler.KeywordsWordList[Section];
  210. if WordList = '' then { CustomMessages }
  211. Exit;
  212. if IsParamSection then
  213. AMemo.SetAutoCompleteFillupChars(':')
  214. else
  215. AMemo.SetAutoCompleteFillupChars('=');
  216. end;
  217. end;
  218. end;
  219. end;
  220. AMemo.ShowAutoComplete(CharsBefore, WordList);
  221. end;
  222. procedure TMainFormAutoCompleteAndCallTipsHelper._UpdateCallTipFunctionDefinition(const AMemo: TScintEdit;
  223. const Pos: Integer { = -1 });
  224. begin
  225. { Based on SciTE 5.50's SciTEBase::FillFunctionDefinition }
  226. if Pos > 0 then
  227. FCallTipState.LastPosCallTip := Pos;
  228. // Should get current api definition
  229. var FunctionDefinition := FMemosStyler.GetScriptFunctionDefinition(FCallTipState.ClassOrRecordMember, FCallTipState.CurrentCallTipWord, FCallTipState.CurrentCallTip, FCallTipState.MaxCallTips);
  230. if ((FCallTipState.MaxCallTips = 1) and FunctionDefinition.HasParams) or //if there's a single definition then only show if it has a parameter
  231. (FCallTipState.MaxCallTips > 1) then begin //if there's multiple then show always just like MemoHintShow, so even the one without parameters if it exists
  232. FCallTipState.FunctionDefinition := FunctionDefinition.ScriptFuncWithoutHeader;
  233. if FCallTipState.MaxCallTips > 1 then
  234. FCallTipState.FunctionDefinition := AnsiString(Format(#1'%d of %d'#2'%s', [FCallTipState.CurrentCallTip+1, FCallTipState.MaxCallTips, FCallTipState.FunctionDefinition]));
  235. AMemo.ShowCallTip(FCallTipState.LastPosCallTip - Length(FCallTipState.CurrentCallTipWord), FCallTipState.FunctionDefinition);
  236. _ContinueCallTip(AMemo);
  237. end;
  238. end;
  239. procedure TMainFormAutoCompleteAndCallTipsHelper._InitiateCallTip(const AMemo: TScintEdit; const Key: AnsiChar);
  240. begin
  241. var Pos := AMemo.CaretPosition;
  242. if (FMemosStyler.GetSectionFromLineState(AMemo.Lines.State[AMemo.GetLineFromPosition(Pos)]) <> scCode) or
  243. ((Key <> #0) and not _InitiateAutoCompleteOrCallTipAllowedAtPos(AMemo,
  244. AMemo.GetPositionFromLine(AMemo.GetLineFromPosition(Pos)),
  245. AMemo.GetPositionBefore(Pos))) then
  246. Exit;
  247. { Based on SciTE 5.50's SciTEBase::StartAutoComplete }
  248. FCallTipState.CurrentCallTip := 0;
  249. FCallTipState.CurrentCallTipWord := '';
  250. var Line := AMemo.CaretLineText;
  251. var Current := AMemo.CaretPositionInLine;
  252. var CallTipWordCharacters := AMemo.WordCharsAsSet;
  253. {$ZEROBASEDSTRINGS ON}
  254. repeat
  255. var Braces := 0;
  256. while ((Current > 0) and ((Braces <> 0) or not (Line[Current-1] = '('))) do begin
  257. if Line[Current-1] = '(' then
  258. Dec(Braces)
  259. else if Line[Current-1] = ')' then
  260. Inc(Braces);
  261. Dec(Current);
  262. Dec(Pos);
  263. end;
  264. if Current > 0 then begin
  265. Dec(Current);
  266. Dec(Pos);
  267. end else
  268. Break;
  269. while (Current > 0) and (Line[Current-1] <= ' ') do begin
  270. Dec(Current);
  271. Dec(Pos);
  272. end
  273. until not ((Current > 0) and not CharInSet(Line[Current-1], CallTipWordCharacters));
  274. {$ZEROBASEDSTRINGS OFF}
  275. if Current <= 0 then
  276. Exit;
  277. FCallTipState.StartCallTipWord := Current - 1;
  278. {$ZEROBASEDSTRINGS ON}
  279. while (FCallTipState.StartCallTipWord > 0) and CharInSet(Line[FCallTipState.StartCallTipWord-1], CallTipWordCharacters) do
  280. Dec(FCallTipState.StartCallTipWord);
  281. FCallTipState.ClassOrRecordMember := (FCallTipState.StartCallTipWord > 0) and (Line[FCallTipState.StartCallTipWord-1] = '.');
  282. {$ZEROBASEDSTRINGS OFF}
  283. SetLength(Line, Current);
  284. FCallTipState.CurrentCallTipWord := Line.Substring(FCallTipState.StartCallTipWord); { Substring is zero-based }
  285. FCallTipState.FunctionDefinition := '';
  286. _UpdateCallTipFunctionDefinition(AMemo, Pos);
  287. end;
  288. procedure TMainFormAutoCompleteAndCallTipsHelper._ContinueCallTip(const AMemo: TScintEdit);
  289. begin
  290. { Based on SciTE 5.50's SciTEBase::ContinueCallTip }
  291. var Line := AMemo.CaretLineText;
  292. var Current := AMemo.CaretPositionInLine;
  293. var Braces := 0;
  294. var Commas := 0;
  295. for var I := FCallTipState.StartCallTipWord to Current-1 do begin
  296. {$ZEROBASEDSTRINGS ON}
  297. if CharInSet(Line[I], ['(', '[']) then
  298. Inc(Braces)
  299. else if CharInSet(Line[I], [')', ']']) and (Braces > 0) then
  300. Dec(Braces)
  301. else if (Braces = 1) and (Line[I] = ',') then
  302. Inc(Commas);
  303. {$ZEROBASEDSTRINGS OFF}
  304. end;
  305. {$ZEROBASEDSTRINGS ON}
  306. var StartHighlight := 0;
  307. var FunctionDefinition := FCallTipState.FunctionDefinition;
  308. var FunctionDefinitionLength := Length(FunctionDefinition);
  309. while (StartHighlight < FunctionDefinitionLength) and not (FunctionDefinition[StartHighlight] = '(') do
  310. Inc(StartHighlight);
  311. if (StartHighlight < FunctionDefinitionLength) and (FunctionDefinition[StartHighlight] = '(') then
  312. Inc(StartHighlight);
  313. while (StartHighlight < FunctionDefinitionLength) and (Commas > 0) do begin
  314. if FunctionDefinition[StartHighlight] in [',', ';'] then
  315. Dec(Commas);
  316. // If it reached the end of the argument list it means that the user typed in more
  317. // arguments than the ones listed in the calltip
  318. if FunctionDefinition[StartHighlight] = ')' then
  319. Commas := 0
  320. else
  321. Inc(StartHighlight);
  322. end;
  323. if (StartHighlight < FunctionDefinitionLength) and (FunctionDefinition[StartHighlight] in [',', ';']) then
  324. Inc(StartHighlight);
  325. var EndHighlight := StartHighlight;
  326. while (EndHighlight < FunctionDefinitionLength) and not (FunctionDefinition[EndHighlight] in [',', ';']) and not (FunctionDefinition[EndHighlight] = ')') do
  327. Inc(EndHighlight);
  328. {$ZEROBASEDSTRINGS OFF}
  329. AMemo.SetCallTipHighlight(StartHighlight, EndHighlight);
  330. end;
  331. procedure TMainFormAutoCompleteAndCallTipsHelper.AutoCompleteAndCallTipsHandleCharAdded(
  332. const AMemo: TScintEdit; const Ch: AnsiChar);
  333. begin
  334. { Based on SciTE 5.50's SciTEBase::CharAdded but with an altered interaction
  335. between calltips and autocomplete }
  336. var DoAutoComplete := False;
  337. if AMemo.CallTipActive then begin
  338. if Ch = ')' then begin
  339. Dec(FCallTipState.BraceCount);
  340. if FCallTipState.BraceCount < 1 then
  341. AMemo.CancelCallTip
  342. else if FOptions.AutoCallTips then
  343. _InitiateCallTip(AMemo, Ch);
  344. end else if Ch = '(' then begin
  345. Inc(FCallTipState.BraceCount);
  346. if FOptions.AutoCallTips then
  347. _InitiateCallTip(AMemo, Ch);
  348. end else
  349. _ContinueCallTip(AMemo);
  350. end else if AMemo.AutoCompleteActive then begin
  351. if Ch = '(' then begin
  352. Inc(FCallTipState.BraceCount);
  353. if FOptions.AutoCallTips then begin
  354. _InitiateCallTip(AMemo, Ch);
  355. if not AMemo.CallTipActive then begin
  356. { Normally the calltip activation means any active autocompletion gets
  357. cancelled by Scintilla but if the current word has no call tip then
  358. we should make sure ourselves that the added brace still cancels
  359. the currently active autocompletion }
  360. DoAutoComplete := True;
  361. end;
  362. end;
  363. end else if Ch = ')' then
  364. Dec(FCallTipState.BraceCount)
  365. else
  366. DoAutoComplete := True;
  367. end else if Ch = '(' then begin
  368. FCallTipState.BraceCount := 1;
  369. if FOptions.AutoCallTips then
  370. _InitiateCallTip(AMemo, Ch);
  371. end else
  372. DoAutoComplete := True;
  373. if DoAutoComplete then begin
  374. case Ch of
  375. 'A'..'Z', 'a'..'z', '_', '#', '{', '[', '<', '0'..'9':
  376. if not AMemo.AutoCompleteActive and FOptions.AutoAutoComplete and not (Ch in ['0'..'9']) then
  377. InitiateAutoComplete(AMemo, Ch);
  378. else
  379. var RestartAutoComplete := (Ch in [' ', '.']) and
  380. (FOptions.AutoAutoComplete or AMemo.AutoCompleteActive);
  381. AMemo.CancelAutoComplete;
  382. if RestartAutoComplete then
  383. InitiateAutoComplete(AMemo, Ch);
  384. end;
  385. end;
  386. end;
  387. procedure TMainFormAutoCompleteAndCallTipsHelper.CallTipsHandleArrowClick(const AMemo: TScintEdit;
  388. const Up: Boolean);
  389. begin
  390. { Based on SciTE 5.50's SciTEBase::Notify SA::Notification::CallTipClick }
  391. if Up and (FCallTipState.CurrentCallTip > 0) then begin
  392. Dec(FCallTipState.CurrentCallTip);
  393. _UpdateCallTipFunctionDefinition(AMemo);
  394. end else if not Up and (FCallTipState.CurrentCallTip + 1 < FCallTipState.MaxCallTips) then begin
  395. Inc(FCallTipState.CurrentCallTip);
  396. _UpdateCallTipFunctionDefinition(AMemo);
  397. end;
  398. end;
  399. procedure TMainFormAutoCompleteAndCallTipsHelper.CallTipsHandleCtrlSpace(const AMemo: TScintEdit);
  400. begin
  401. { Based on SciTE 5.50's SciTEBase::MenuCommand IDM_SHOWCALLTIP }
  402. if AMemo.CallTipActive then begin
  403. FCallTipState.CurrentCallTip := IfThen(FCallTipState.CurrentCallTip + 1 = FCallTipState.MaxCallTips, 0, FCallTipState.CurrentCallTip + 1);
  404. _UpdateCallTipFunctionDefinition(AMemo);
  405. end else begin
  406. FCallTipState.BraceCount := 1; { Missing in SciTE, see https://sourceforge.net/p/scintilla/bugs/2446/ }
  407. _InitiateCallTip(AMemo, #0);
  408. end;
  409. end;
  410. end.