IDE.MainForm.ScintHelper.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. unit IDE.MainForm.ScintHelper;
  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 - Scintilla helper which has the auto complete and call tips 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.AutoCompleteAndCallTipsHelper;
  15. type
  16. TMainFormScintHelper = class helper(TMainFormAutoCompleteAndCallTipsHelper) for TMainForm
  17. procedure SimplifySelection(const AMemo: TScintEdit);
  18. procedure AddCursorUpOrDown(const AMemo: TScintEdit; const Up: Boolean);
  19. procedure AddCursorsToLineEnds(const AMemo: TScintEdit);
  20. function MultipleSelectionPasteFromClipboard(const AMemo: TScintEdit): Boolean;
  21. procedure ToggleLinesComment(const AMemo: TScintEdit);
  22. procedure SelectAllFindMatches(const AMemo: TScintEdit);
  23. end;
  24. implementation
  25. uses
  26. SysUtils, Clipbrd, Math,
  27. ScintInt,
  28. IDE.HelperFunc, IDE.ScintStylerInnoSetup;
  29. procedure TMainFormScintHelper.SimplifySelection(const AMemo: TScintEdit);
  30. begin
  31. { The built in Esc (SCI_CANCEL) simply drops all additional selections
  32. and does not empty the main selection, It doesn't matter if Esc is
  33. pressed once or twice. Implement our own behaviour, same as VSCode.
  34. Also see https://github.com/microsoft/vscode/issues/118835. }
  35. if AMemo.SelectionCount > 1 then
  36. AMemo.RemoveAdditionalSelections
  37. else if not AMemo.SelEmpty then
  38. AMemo.SetEmptySelection;
  39. AMemo.ScrollCaretIntoView;
  40. end;
  41. procedure TMainFormScintHelper.AddCursorUpOrDown(const AMemo: TScintEdit; const Up: Boolean);
  42. begin
  43. { Does not try to keep the main selection. }
  44. var Selections: TScintCaretAndAnchorList := nil;
  45. var VirtualSpaces: TScintCaretAndAnchorList := nil;
  46. try
  47. Selections := TScintCaretAndAnchorList.Create;
  48. VirtualSpaces := TScintCaretAndAnchorList.Create;
  49. { Get all the virtual spaces as well before we start doing modifications }
  50. AMemo.GetSelections(Selections, VirtualSpaces);
  51. for var I := 0 to Selections.Count-1 do begin
  52. var Selection := Selections[I];
  53. var LineCaret := AMemo.GetLineFromPosition(Selection.CaretPos);
  54. var LineAnchor := AMemo.GetLineFromPosition(Selection.AnchorPos);
  55. if LineCaret = LineAnchor then begin
  56. { Add selection with same caret and anchor offsets one line up or down. }
  57. var OtherLine := LineCaret + IfThen(Up, -1, 1);
  58. if (OtherLine < 0) or (OtherLine >= AMemo.Lines.Count) then
  59. Continue { Already at the top or bottom, can't add }
  60. else begin
  61. var LineStartPos := AMemo.GetPositionFromLine(LineCaret);
  62. var CaretCharacterCount := AMemo.GetCharacterCount(LineStartPos, Selection.CaretPos) + VirtualSpaces[I].CaretPos;
  63. var AnchorCharacterCount := AMemo.GetCharacterCount(LineStartPos, Selection.AnchorPos) + VirtualSpaces[I].AnchorPos;
  64. var OtherLineStart := AMemo.GetPositionFromLine(OtherLine);
  65. var MaxCharacterCount := AMemo.GetCharacterCount(OtherLineStart, AMemo.GetLineEndPosition(OtherLine));
  66. var NewCaretCharacterCount := CaretCharacterCount;
  67. //var NewCaretVirtualSpace := 0;
  68. var NewAnchorCharacterCount := AnchorCharacterCount;
  69. //var NewAnchorVirtualSpace := 0;
  70. if NewCaretCharacterCount > MaxCharacterCount then begin
  71. //NewCaretVirtualSpace := NewCaretCharacterCount - MaxCharacterCount;
  72. NewCaretCharacterCount := MaxCharacterCount;
  73. end;
  74. if NewAnchorCharacterCount > MaxCharacterCount then begin
  75. //NewAnchorVirtualSpace := NewAnchorCharacterCount - MaxCharacterCount;
  76. NewAnchorCharacterCount := MaxCharacterCount;
  77. end;
  78. var NewSelection: TScintCaretAndAnchor;
  79. NewSelection.CaretPos := AMemo.GetPositionRelative(OtherLineStart, NewCaretCharacterCount);
  80. NewSelection.AnchorPos := AMemo.GetPositionRelative(OtherLineStart, NewAnchorCharacterCount);
  81. { AddSelection trims selections except for the main selection so
  82. we need to check that ourselves unfortunately. Not doing a check
  83. gives a problem when you AddCursor two times starting with an
  84. empty single selection. The result will be 4 cursors, with 2 of
  85. them in the same place. The check below fixes this but not
  86. other cases when there's only partial overlap and Scintilla still
  87. behaves weird. The check also doesn't handle virtual space which
  88. is why we ultimately don't set virtual space: it leads to duplicate
  89. selections. }
  90. var MainSelection := AMemo.Selection;
  91. if not NewSelection.Range.Within(MainSelection) then begin
  92. AMemo.AddSelection(NewSelection.CaretPos, NewSelection.AnchorPos);
  93. { if svsUserAccessible in AMemo.VirtualSpaceOptions then begin
  94. var MainSel := AMemo.MainSelection;
  95. AMemo.SelectionCaretVirtualSpace[MainSel] := NewCaretVirtualSpace;
  96. AMemo.SelectionAnchorVirtualSpace[MainSel] := NewAnchorVirtualSpace;
  97. end; }
  98. end;
  99. end;
  100. end else begin
  101. { Extend multiline selection up or down. This is not the same as
  102. LineExtendUp/Down because those can shrink instead of extend. }
  103. var CaretBeforeAnchor := Selection.CaretPos < Selection.AnchorPos;
  104. var Down := not Up;
  105. var LineStartOrEnd, StartOrEndPos, VirtualSpace: Integer;
  106. { Does it start (when going up) or end (when going down) at the caret or the anchor? }
  107. if (Up and CaretBeforeAnchor) or (Down and not CaretBeforeAnchor) then begin
  108. LineStartOrEnd := LineCaret;
  109. StartOrEndPos := Selection.CaretPos;
  110. VirtualSpace := VirtualSpaces[I].CaretPos;
  111. end else begin
  112. LineStartOrEnd := LineAnchor;
  113. StartOrEndPos := Selection.AnchorPos;
  114. VirtualSpace := VirtualSpaces[I].AnchorPos;
  115. end;
  116. var NewStartOrEndPos: Integer;
  117. var NewVirtualSpace := 0;
  118. { Go up or down one line or to the start or end of the document }
  119. if (Up and (LineStartOrEnd > 0)) or (Down and (LineStartOrEnd < AMemo.Lines.Count-1)) then begin
  120. var CharacterCount := AMemo.GetCharacterCount(AMemo.GetPositionFromLine(LineStartOrEnd), StartOrEndPos) + VirtualSpace;
  121. var OtherLine := LineStartOrEnd + IfThen(Up, -1, 1);
  122. var OtherLineStart := AMemo.GetPositionFromLine(OtherLine);
  123. var MaxCharacterCount := AMemo.GetCharacterCount(OtherLineStart, AMemo.GetLineEndPosition(OtherLine));
  124. var NewCharacterCount := CharacterCount;
  125. if NewCharacterCount > MaxCharacterCount then begin
  126. NewVirtualSpace := NewCharacterCount - MaxCharacterCount;
  127. NewCharacterCount := MaxCharacterCount;
  128. end;
  129. NewStartOrEndPos := AMemo.GetPositionRelative(OtherLineStart, NewCharacterCount);
  130. end else
  131. NewStartOrEndPos := IfThen(Up, 0, AMemo.GetPositionFromLine(AMemo.Lines.Count));
  132. { Move the caret or the anchor up or down to extend the selection }
  133. if (Up and CaretBeforeAnchor) or (Down and not CaretBeforeAnchor) then begin
  134. AMemo.SelectionCaretPosition[I] := NewStartOrEndPos;
  135. if svsUserAccessible in AMemo.VirtualSpaceOptions then
  136. AMemo.SelectionCaretVirtualSpace[I] := NewVirtualSpace;
  137. end else begin
  138. AMemo.SelectionAnchorPosition[I] := NewStartOrEndPos;
  139. if svsUserAccessible in AMemo.VirtualSpaceOptions then
  140. AMemo.SelectionAnchorVirtualSpace[I] := NewVirtualSpace;
  141. end;
  142. end;
  143. end;
  144. finally
  145. VirtualSpaces.Free;
  146. Selections.Free;
  147. end;
  148. end;
  149. procedure TMainFormScintHelper.AddCursorsToLineEnds(const AMemo: TScintEdit);
  150. begin
  151. { Does not try to keep the main selection. Otherwise behaves the same as
  152. observed in Visual Studio Code, see comments. }
  153. var Selections: TScintCaretAndAnchorList := nil;
  154. var VirtualSpaces: TScintCaretAndAnchorList := nil;
  155. try
  156. Selections := TScintCaretAndAnchorList.Create;
  157. VirtualSpaces := TScintCaretAndAnchorList.Create;
  158. AMemo.GetSelections(Selections, VirtualSpaces);
  159. { First remove all empty selections }
  160. for var I := Selections.Count-1 downto 0 do begin
  161. var Selection := Selections[I];
  162. var VirtualSpace := VirtualSpaces[I];
  163. if (Selection.CaretPos + VirtualSpace.CaretPos) =
  164. (Selection.AnchorPos + VirtualSpace.AnchorPos) then begin
  165. Selections.Delete(I);
  166. VirtualSpaces.Delete(I);
  167. end;
  168. end;
  169. { If all selections were empty do nothing }
  170. if Selections.Count = 0 then
  171. Exit;
  172. { Handle non empty selections }
  173. for var I := Selections.Count-1 downto 0 do begin
  174. var Selection := Selections[I];
  175. var Line1 := AMemo.GetLineFromPosition(Selection.CaretPos);
  176. var Line2 := AMemo.GetLineFromPosition(Selection.AnchorPos);
  177. var SelSingleLine := Line1 = Line2;
  178. if SelSingleLine then begin
  179. { Single line selections are updated into empty selection at end of selection }
  180. var VirtualSpace := VirtualSpaces[I];
  181. if Selection.CaretPos + VirtualSpace.CaretPos > Selection.AnchorPos + VirtualSpace.AnchorPos then begin
  182. Selection.AnchorPos := Selection.CaretPos;
  183. VirtualSpace.AnchorPos := VirtualSpace.CaretPos;
  184. end else begin
  185. Selection.CaretPos := Selection.AnchorPos;
  186. VirtualSpace.CaretPos := VirtualSpace.AnchorPos;
  187. end;
  188. Selections[I] := Selection;
  189. VirtualSpaces[I] := VirtualSpace;
  190. end else begin
  191. { Multiline selections are replaced by empty selections at each end of line }
  192. if Line1 > Line2 then begin
  193. var TmpLine := Line1;
  194. Line1 := Line2;
  195. Line2 := TmpLine;
  196. end;
  197. { Ignore last line if the selection doesn't really select anything on that line }
  198. if Selection.Range.EndPos = AMemo.GetPositionFromLine(Line2) then
  199. Dec(Line2);
  200. for var Line := Line1 to Line2 do begin
  201. Selection.CaretPos := AMemo.GetLineEndPosition(Line);
  202. Selection.AnchorPos := Selection.CaretPos;
  203. Selections.Add(Selection);
  204. VirtualSpaces.Add(TScintCaretAndAnchor.Create(0, 0));
  205. end;
  206. Selections.Delete(I);
  207. VirtualSpaces.Delete(I);
  208. end;
  209. end;
  210. { Send updated selections to memo }
  211. for var I := 0 to Selections.Count-1 do begin
  212. var Selection := Selections[I];
  213. var VirtualSpace := VirtualSpaces[I];
  214. if I = 0 then
  215. AMemo.SetSingleSelection(Selection.CaretPos, Selection.AnchorPos)
  216. else
  217. AMemo.AddSelection(Selection.CaretPos, Selection.AnchorPos);
  218. AMemo.SelectionCaretVirtualSpace[I] := VirtualSpaces[I].CaretPos;
  219. AMemo.SelectionAnchorVirtualSpace[I] := VirtualSpaces[I].AnchorPos;
  220. end;
  221. finally
  222. VirtualSpaces.Free;
  223. Selections.Free;
  224. end;
  225. end;
  226. function TMainFormScintHelper.MultipleSelectionPasteFromClipboard(const AMemo: TScintEdit): Boolean;
  227. begin
  228. { Scintilla doesn't yet properly support multiple selection paste. Handle it
  229. here, just like VS and VSCode do: if there's multiple selections and the paste
  230. text has the same amount of lines then paste 1 line per selection. Do this even
  231. if the paste text is marked as rectangular. Otherwise (so no match between
  232. the selection count and the line count) paste all lines into each selection.
  233. For the latter we don't need handling here: this is Scintilla's default
  234. behaviour if SC_MULTIPASTE_EACH is on. }
  235. Result := False;
  236. var SelectionCount := AMemo.SelectionCount;
  237. if SelectionCount > 1 then begin
  238. var PasteLines := Clipboard.AsText.Replace(#13#10, #13).Split([#13, #10]);
  239. if SelectionCount = Length(PasteLines) then begin
  240. AMemo.BeginUndoAction;
  241. try
  242. for var I := 0 to SelectionCount-1 do begin
  243. var StartPos := AMemo.SelectionStartPosition[I]; { Can't use AMemo.GetSelections because each paste can update other selections }
  244. var EndPos := AMemo.SelectionEndPosition[I];
  245. AMemo.ReplaceTextRange(StartPos, EndPos, PasteLines[I], srmMinimal);
  246. { Update the selection to an empty selection at the end of the inserted
  247. text, just like ReplaceMainSelText }
  248. var Pos := AMemo.Target.EndPos; { ReplaceTextRange updates the target }
  249. AMemo.SelectionCaretPosition[I] := Pos;
  250. AMemo.SelectionAnchorPosition[I] := Pos;
  251. end;
  252. { Be like SCI_PASTE }
  253. AMemo.ChooseCaretX;
  254. AMemo.ScrollCaretIntoView;
  255. finally
  256. AMemo.EndUndoAction;
  257. end;
  258. Result := True;
  259. end;
  260. end;
  261. end;
  262. procedure TMainFormScintHelper.ToggleLinesComment(const AMemo: TScintEdit);
  263. begin
  264. { Based on SciTE 5.50's SciTEBase::StartBlockComment - only toggles comments
  265. for the main selection }
  266. var Selection := AMemo.Selection;
  267. var CaretPosition := AMemo.CaretPosition;
  268. // checking if caret is located in _beginning_ of selected block
  269. var MoveCaret := CaretPosition < Selection.EndPos;
  270. var SelStartLine := AMemo.GetLineFromPosition(Selection.StartPos);
  271. var SelEndLine := AMemo.GetLineFromPosition(Selection.EndPos);
  272. var Lines := SelEndLine - SelStartLine;
  273. var FirstSelLineStart := AMemo.GetPositionFromLine(SelStartLine);
  274. // "caret return" is part of the last selected line
  275. if (Lines > 0) and (Selection.EndPos = AMemo.GetPositionFromLine(SelEndLine)) then
  276. Dec(SelEndLine);
  277. { We rely on the styler to identify [Code] section lines, but we
  278. may be searching into areas that haven't been styled yet }
  279. AMemo.StyleNeeded(Selection.EndPos);
  280. AMemo.BeginUndoAction;
  281. try
  282. var LastLongCommentLength := 0;
  283. for var I := SelStartLine to SelEndLine do begin
  284. var LineIndent := AMemo.GetLineIndentPosition(I);
  285. var LineEnd := AMemo.GetLineEndPosition(I);
  286. var LineBuf := AMemo.GetTextRange(LineIndent, LineEnd);
  287. // empty lines are not commented
  288. if LineBuf = '' then
  289. Continue;
  290. var Comment: String;
  291. if LineBuf.StartsWith('//') or
  292. (FMemosStyler.GetSectionFromLineState(AMemo.Lines.State[I]) = scCode) then
  293. Comment := '//'
  294. else
  295. Comment := ';';
  296. var LongComment := Comment + ' ';
  297. LastLongCommentLength := Length(LongComment);
  298. if LineBuf.StartsWith(Comment) then begin
  299. var CommentLength := Length(Comment);
  300. if LineBuf.StartsWith(LongComment) then begin
  301. // Removing comment with space after it.
  302. CommentLength := Length(LongComment);
  303. end;
  304. AMemo.Selection := TScintRange.Create(LineIndent, LineIndent + CommentLength);
  305. AMemo.SelText := '';
  306. if I = SelStartLine then // is this the first selected line?
  307. Dec(Selection.StartPos, CommentLength);
  308. Dec(Selection.EndPos, CommentLength); // every iteration
  309. Continue;
  310. end;
  311. if I = SelStartLine then // is this the first selected line?
  312. Inc(Selection.StartPos, Length(LongComment));
  313. Inc(Selection.EndPos, Length(LongComment)); // every iteration
  314. AMemo.Call(SCI_INSERTTEXT, LineIndent, AMemo.ConvertStringToRawString(LongComment));
  315. end;
  316. // after uncommenting selection may promote itself to the lines
  317. // before the first initially selected line;
  318. // another problem - if only comment symbol was selected;
  319. if Selection.StartPos < FirstSelLineStart then begin
  320. if Selection.StartPos >= Selection.EndPos - (LastLongCommentLength - 1) then
  321. Selection.EndPos := FirstSelLineStart;
  322. Selection.StartPos := FirstSelLineStart;
  323. end;
  324. if MoveCaret then begin
  325. // moving caret to the beginning of selected block
  326. AMemo.CaretPosition := Selection.EndPos;
  327. AMemo.CaretPositionWithSelectFromAnchor := Selection.StartPos;
  328. end else
  329. AMemo.Selection := Selection;
  330. finally
  331. AMemo.EndUndoAction;
  332. end;
  333. end;
  334. procedure TMainFormScintHelper.SelectAllFindMatches(const AMemo: TScintEdit);
  335. begin
  336. var StartPos := 0;
  337. var EndPos := AMemo.RawTextLength;
  338. var FoundRange: TScintRange;
  339. var ClosestSelection := -1;
  340. var ClosestSelectionDistance := 0; { Silence compiler }
  341. var CaretPos := AMemo.CaretPosition;
  342. while (StartPos < EndPos) and
  343. AMemo.FindText(StartPos, EndPos, FLastFindText,
  344. FindOptionsToSearchOptions(FLastFindOptions, FLastFindRegEx), FoundRange) do begin
  345. if StartPos = 0 then
  346. AMemo.SetSingleSelection(FoundRange.EndPos, FoundRange.StartPos)
  347. else
  348. AMemo.AddSelection(FoundRange.EndPos, FoundRange.StartPos);
  349. var Distance := Abs(CaretPos-FoundRange.EndPos);
  350. if (ClosestSelection = -1) or (Distance < ClosestSelectionDistance) then begin
  351. ClosestSelection := AMemo.SelectionCount-1;
  352. ClosestSelectionDistance := Distance;
  353. end;
  354. StartPos := FoundRange.EndPos;
  355. end;
  356. if ClosestSelection <> -1 then begin
  357. AMemo.MainSelection := ClosestSelection;
  358. AMemo.ScrollCaretIntoView;
  359. end;
  360. end;
  361. end.