| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- unit IDE.MainForm.ScintHelper;
- {
- Inno Setup
- Copyright (C) 1997-2025 Jordan Russell
- Portions by Martijn Laan
- For conditions of distribution and use, see LICENSE.TXT.
- Compiler form - Scintilla helper which has the auto complete and call tips helper as ancestor
- Not used by MainForm: it uses IDE.MainForm.FinalHelper instead
- }
- interface
- uses
- Menus,
- ScintEdit,
- IDE.MainForm, IDE.MainForm.AutoCompleteAndCallTipsHelper;
- type
- TMainFormScintHelper = class helper(TMainFormAutoCompleteAndCallTipsHelper) for TMainForm
- procedure SimplifySelection(const AMemo: TScintEdit);
- procedure AddCursorUpOrDown(const AMemo: TScintEdit; const Up: Boolean);
- procedure AddCursorsToLineEnds(const AMemo: TScintEdit);
- function MultipleSelectionPasteFromClipboard(const AMemo: TScintEdit): Boolean;
- procedure ToggleLinesComment(const AMemo: TScintEdit);
- procedure SelectAllFindMatches(const AMemo: TScintEdit);
- end;
- implementation
- uses
- SysUtils, Clipbrd, Math,
- ScintInt,
- IDE.HelperFunc, IDE.ScintStylerInnoSetup;
- procedure TMainFormScintHelper.SimplifySelection(const AMemo: TScintEdit);
- begin
- { The built in Esc (SCI_CANCEL) simply drops all additional selections
- and does not empty the main selection, It doesn't matter if Esc is
- pressed once or twice. Implement our own behaviour, same as VSCode.
- Also see https://github.com/microsoft/vscode/issues/118835. }
- if AMemo.SelectionCount > 1 then
- AMemo.RemoveAdditionalSelections
- else if not AMemo.SelEmpty then
- AMemo.SetEmptySelection;
- AMemo.ScrollCaretIntoView;
- end;
- procedure TMainFormScintHelper.AddCursorUpOrDown(const AMemo: TScintEdit; const Up: Boolean);
- begin
- { Does not try to keep the main selection. }
- var Selections: TScintCaretAndAnchorList := nil;
- var VirtualSpaces: TScintCaretAndAnchorList := nil;
- try
- Selections := TScintCaretAndAnchorList.Create;
- VirtualSpaces := TScintCaretAndAnchorList.Create;
- { Get all the virtual spaces as well before we start doing modifications }
- AMemo.GetSelections(Selections, VirtualSpaces);
- for var I := 0 to Selections.Count-1 do begin
- var Selection := Selections[I];
- var LineCaret := AMemo.GetLineFromPosition(Selection.CaretPos);
- var LineAnchor := AMemo.GetLineFromPosition(Selection.AnchorPos);
- if LineCaret = LineAnchor then begin
- { Add selection with same caret and anchor offsets one line up or down. }
- var OtherLine := LineCaret + IfThen(Up, -1, 1);
- if (OtherLine < 0) or (OtherLine >= AMemo.Lines.Count) then
- Continue { Already at the top or bottom, can't add }
- else begin
- var LineStartPos := AMemo.GetPositionFromLine(LineCaret);
- var CaretCharacterCount := AMemo.GetCharacterCount(LineStartPos, Selection.CaretPos) + VirtualSpaces[I].CaretPos;
- var AnchorCharacterCount := AMemo.GetCharacterCount(LineStartPos, Selection.AnchorPos) + VirtualSpaces[I].AnchorPos;
- var OtherLineStart := AMemo.GetPositionFromLine(OtherLine);
- var MaxCharacterCount := AMemo.GetCharacterCount(OtherLineStart, AMemo.GetLineEndPosition(OtherLine));
- var NewCaretCharacterCount := CaretCharacterCount;
- var NewCaretVirtualSpace := 0;
- var NewAnchorCharacterCount := AnchorCharacterCount;
- var NewAnchorVirtualSpace := 0;
- if NewCaretCharacterCount > MaxCharacterCount then begin
- NewCaretVirtualSpace := NewCaretCharacterCount - MaxCharacterCount;
- NewCaretCharacterCount := MaxCharacterCount;
- end;
- if NewAnchorCharacterCount > MaxCharacterCount then begin
- NewAnchorVirtualSpace := NewAnchorCharacterCount - MaxCharacterCount;
- NewAnchorCharacterCount := MaxCharacterCount;
- end;
- var NewSelection: TScintCaretAndAnchor;
- NewSelection.CaretPos := AMemo.GetPositionRelative(OtherLineStart, NewCaretCharacterCount);
- NewSelection.AnchorPos := AMemo.GetPositionRelative(OtherLineStart, NewAnchorCharacterCount);
- { AddSelection trims selections except for the main selection so
- we need to check that ourselves unfortunately. Not doing a check
- gives a problem when you AddCursor two times starting with an
- empty single selection. The result will be 4 cursors, with 2 of
- them in the same place. The check below fixes this but not
- other cases when there's only partial overlap and Scintilla still
- behaves weird. The check also doesn't handle virtual space which
- is why we ultimately don't set virtual space: it leads to duplicate
- selections. }
- var MainSelection := AMemo.Selection;
- if not NewSelection.Range.Within(MainSelection) then begin
- AMemo.AddSelection(NewSelection.CaretPos, NewSelection.AnchorPos);
- { if svsUserAccessible in AMemo.VirtualSpaceOptions then begin
- var MainSel := AMemo.MainSelection;
- AMemo.SelectionCaretVirtualSpace[MainSel] := NewCaretVirtualSpace;
- AMemo.SelectionAnchorVirtualSpace[MainSel] := NewAnchorVirtualSpace;
- end; }
- end;
- end;
- end else begin
- { Extend multiline selection up or down. This is not the same as
- LineExtendUp/Down because those can shrink instead of extend. }
- var CaretBeforeAnchor := Selection.CaretPos < Selection.AnchorPos;
- var Down := not Up;
- var LineStartOrEnd, StartOrEndPos, VirtualSpace: Integer;
- { Does it start (when going up) or end (when going down) at the caret or the anchor? }
- if (Up and CaretBeforeAnchor) or (Down and not CaretBeforeAnchor) then begin
- LineStartOrEnd := LineCaret;
- StartOrEndPos := Selection.CaretPos;
- VirtualSpace := VirtualSpaces[I].CaretPos;
- end else begin
- LineStartOrEnd := LineAnchor;
- StartOrEndPos := Selection.AnchorPos;
- VirtualSpace := VirtualSpaces[I].AnchorPos;
- end;
- var NewStartOrEndPos: Integer;
- var NewVirtualSpace := 0;
- { Go up or down one line or to the start or end of the document }
- if (Up and (LineStartOrEnd > 0)) or (Down and (LineStartOrEnd < AMemo.Lines.Count-1)) then begin
- var CharacterCount := AMemo.GetCharacterCount(AMemo.GetPositionFromLine(LineStartOrEnd), StartOrEndPos) + VirtualSpace;
- var OtherLine := LineStartOrEnd + IfThen(Up, -1, 1);
- var OtherLineStart := AMemo.GetPositionFromLine(OtherLine);
- var MaxCharacterCount := AMemo.GetCharacterCount(OtherLineStart, AMemo.GetLineEndPosition(OtherLine));
- var NewCharacterCount := CharacterCount;
- if NewCharacterCount > MaxCharacterCount then begin
- NewVirtualSpace := NewCharacterCount - MaxCharacterCount;
- NewCharacterCount := MaxCharacterCount;
- end;
- NewStartOrEndPos := AMemo.GetPositionRelative(OtherLineStart, NewCharacterCount);
- end else
- NewStartOrEndPos := IfThen(Up, 0, AMemo.GetPositionFromLine(AMemo.Lines.Count));
- { Move the caret or the anchor up or down to extend the selection }
- if (Up and CaretBeforeAnchor) or (Down and not CaretBeforeAnchor) then begin
- AMemo.SelectionCaretPosition[I] := NewStartOrEndPos;
- if svsUserAccessible in AMemo.VirtualSpaceOptions then
- AMemo.SelectionCaretVirtualSpace[I] := NewVirtualSpace;
- end else begin
- AMemo.SelectionAnchorPosition[I] := NewStartOrEndPos;
- if svsUserAccessible in AMemo.VirtualSpaceOptions then
- AMemo.SelectionAnchorVirtualSpace[I] := NewVirtualSpace;
- end;
- end;
- end;
- finally
- VirtualSpaces.Free;
- Selections.Free;
- end;
- end;
- procedure TMainFormScintHelper.AddCursorsToLineEnds(const AMemo: TScintEdit);
- begin
- { Does not try to keep the main selection. Otherwise behaves the same as
- observed in Visual Studio Code, see comments. }
- var Selections: TScintCaretAndAnchorList := nil;
- var VirtualSpaces: TScintCaretAndAnchorList := nil;
- try
- Selections := TScintCaretAndAnchorList.Create;
- VirtualSpaces := TScintCaretAndAnchorList.Create;
- AMemo.GetSelections(Selections, VirtualSpaces);
- { First remove all empty selections }
- for var I := Selections.Count-1 downto 0 do begin
- var Selection := Selections[I];
- var VirtualSpace := VirtualSpaces[I];
- if (Selection.CaretPos + VirtualSpace.CaretPos) =
- (Selection.AnchorPos + VirtualSpace.AnchorPos) then begin
- Selections.Delete(I);
- VirtualSpaces.Delete(I);
- end;
- end;
- { If all selections were empty do nothing }
- if Selections.Count = 0 then
- Exit;
- { Handle non empty selections }
- for var I := Selections.Count-1 downto 0 do begin
- var Selection := Selections[I];
- var Line1 := AMemo.GetLineFromPosition(Selection.CaretPos);
- var Line2 := AMemo.GetLineFromPosition(Selection.AnchorPos);
- var SelSingleLine := Line1 = Line2;
- if SelSingleLine then begin
- { Single line selections are updated into empty selection at end of selection }
- var VirtualSpace := VirtualSpaces[I];
- if Selection.CaretPos + VirtualSpace.CaretPos > Selection.AnchorPos + VirtualSpace.AnchorPos then begin
- Selection.AnchorPos := Selection.CaretPos;
- VirtualSpace.AnchorPos := VirtualSpace.CaretPos;
- end else begin
- Selection.CaretPos := Selection.AnchorPos;
- VirtualSpace.CaretPos := VirtualSpace.AnchorPos;
- end;
- Selections[I] := Selection;
- VirtualSpaces[I] := VirtualSpace;
- end else begin
- { Multiline selections are replaced by empty selections at each end of line }
- if Line1 > Line2 then begin
- var TmpLine := Line1;
- Line1 := Line2;
- Line2 := TmpLine;
- end;
- { Ignore last line if the selection doesn't really select anything on that line }
- if Selection.Range.EndPos = AMemo.GetPositionFromLine(Line2) then
- Dec(Line2);
- for var Line := Line1 to Line2 do begin
- Selection.CaretPos := AMemo.GetLineEndPosition(Line);
- Selection.AnchorPos := Selection.CaretPos;
- Selections.Add(Selection);
- VirtualSpaces.Add(TScintCaretAndAnchor.Create(0, 0));
- end;
- Selections.Delete(I);
- VirtualSpaces.Delete(I);
- end;
- end;
- { Send updated selections to memo }
- for var I := 0 to Selections.Count-1 do begin
- var Selection := Selections[I];
- var VirtualSpace := VirtualSpaces[I];
- if I = 0 then
- AMemo.SetSingleSelection(Selection.CaretPos, Selection.AnchorPos)
- else
- AMemo.AddSelection(Selection.CaretPos, Selection.AnchorPos);
- AMemo.SelectionCaretVirtualSpace[I] := VirtualSpaces[I].CaretPos;
- AMemo.SelectionAnchorVirtualSpace[I] := VirtualSpaces[I].AnchorPos;
- end;
- finally
- VirtualSpaces.Free;
- Selections.Free;
- end;
- end;
- function TMainFormScintHelper.MultipleSelectionPasteFromClipboard(const AMemo: TScintEdit): Boolean;
- begin
- { Scintilla doesn't yet properly support multiple selection paste. Handle it
- here, just like VS and VSCode do: if there's multiple selections and the paste
- text has the same amount of lines then paste 1 line per selection. Do this even
- if the paste text is marked as rectangular. Otherwise (so no match between
- the selection count and the line count) paste all lines into each selection.
- For the latter we don't need handling here: this is Scintilla's default
- behaviour if SC_MULTIPASTE_EACH is on. }
- Result := False;
- var SelectionCount := AMemo.SelectionCount;
- if SelectionCount > 1 then begin
- var PasteLines := Clipboard.AsText.Replace(#13#10, #13).Split([#13, #10]);
- if SelectionCount = Length(PasteLines) then begin
- AMemo.BeginUndoAction;
- try
- for var I := 0 to SelectionCount-1 do begin
- var StartPos := AMemo.SelectionStartPosition[I]; { Can't use AMemo.GetSelections because each paste can update other selections }
- var EndPos := AMemo.SelectionEndPosition[I];
- AMemo.ReplaceTextRange(StartPos, EndPos, PasteLines[I], srmMinimal);
- { Update the selection to an empty selection at the end of the inserted
- text, just like ReplaceMainSelText }
- var Pos := AMemo.Target.EndPos; { ReplaceTextRange updates the target }
- AMemo.SelectionCaretPosition[I] := Pos;
- AMemo.SelectionAnchorPosition[I] := Pos;
- end;
- { Be like SCI_PASTE }
- AMemo.ChooseCaretX;
- AMemo.ScrollCaretIntoView;
- finally
- AMemo.EndUndoAction;
- end;
- Result := True;
- end;
- end;
- end;
- procedure TMainFormScintHelper.ToggleLinesComment(const AMemo: TScintEdit);
- begin
- { Based on SciTE 5.50's SciTEBase::StartBlockComment - only toggles comments
- for the main selection }
- var Selection := AMemo.Selection;
- var CaretPosition := AMemo.CaretPosition;
- // checking if caret is located in _beginning_ of selected block
- var MoveCaret := CaretPosition < Selection.EndPos;
- var SelStartLine := AMemo.GetLineFromPosition(Selection.StartPos);
- var SelEndLine := AMemo.GetLineFromPosition(Selection.EndPos);
- var Lines := SelEndLine - SelStartLine;
- var FirstSelLineStart := AMemo.GetPositionFromLine(SelStartLine);
- // "caret return" is part of the last selected line
- if (Lines > 0) and (Selection.EndPos = AMemo.GetPositionFromLine(SelEndLine)) then
- Dec(SelEndLine);
- { We rely on the styler to identify [Code] section lines, but we
- may be searching into areas that haven't been styled yet }
- AMemo.StyleNeeded(Selection.EndPos);
- AMemo.BeginUndoAction;
- try
- var LastLongCommentLength := 0;
- for var I := SelStartLine to SelEndLine do begin
- var LineIndent := AMemo.GetLineIndentPosition(I);
- var LineEnd := AMemo.GetLineEndPosition(I);
- var LineBuf := AMemo.GetTextRange(LineIndent, LineEnd);
- // empty lines are not commented
- if LineBuf = '' then
- Continue;
- var Comment: String;
- if LineBuf.StartsWith('//') or
- (FMemosStyler.GetSectionFromLineState(AMemo.Lines.State[I]) = scCode) then
- Comment := '//'
- else
- Comment := ';';
- var LongComment := Comment + ' ';
- LastLongCommentLength := Length(LongComment);
- if LineBuf.StartsWith(Comment) then begin
- var CommentLength := Length(Comment);
- if LineBuf.StartsWith(LongComment) then begin
- // Removing comment with space after it.
- CommentLength := Length(LongComment);
- end;
- AMemo.Selection := TScintRange.Create(LineIndent, LineIndent + CommentLength);
- AMemo.SelText := '';
- if I = SelStartLine then // is this the first selected line?
- Dec(Selection.StartPos, CommentLength);
- Dec(Selection.EndPos, CommentLength); // every iteration
- Continue;
- end;
- if I = SelStartLine then // is this the first selected line?
- Inc(Selection.StartPos, Length(LongComment));
- Inc(Selection.EndPos, Length(LongComment)); // every iteration
- AMemo.Call(SCI_INSERTTEXT, LineIndent, AMemo.ConvertStringToRawString(LongComment));
- end;
- // after uncommenting selection may promote itself to the lines
- // before the first initially selected line;
- // another problem - if only comment symbol was selected;
- if Selection.StartPos < FirstSelLineStart then begin
- if Selection.StartPos >= Selection.EndPos - (LastLongCommentLength - 1) then
- Selection.EndPos := FirstSelLineStart;
- Selection.StartPos := FirstSelLineStart;
- end;
- if MoveCaret then begin
- // moving caret to the beginning of selected block
- AMemo.CaretPosition := Selection.EndPos;
- AMemo.CaretPositionWithSelectFromAnchor := Selection.StartPos;
- end else
- AMemo.Selection := Selection;
- finally
- AMemo.EndUndoAction;
- end;
- end;
- procedure TMainFormScintHelper.SelectAllFindMatches(const AMemo: TScintEdit);
- begin
- var StartPos := 0;
- var EndPos := AMemo.RawTextLength;
- var FoundRange: TScintRange;
- var ClosestSelection := -1;
- var ClosestSelectionDistance := 0; { Silence compiler }
- var CaretPos := AMemo.CaretPosition;
- while (StartPos < EndPos) and
- AMemo.FindText(StartPos, EndPos, FLastFindText,
- FindOptionsToSearchOptions(FLastFindOptions, FLastFindRegEx), FoundRange) do begin
- if StartPos = 0 then
- AMemo.SetSingleSelection(FoundRange.EndPos, FoundRange.StartPos)
- else
- AMemo.AddSelection(FoundRange.EndPos, FoundRange.StartPos);
- var Distance := Abs(CaretPos-FoundRange.EndPos);
- if (ClosestSelection = -1) or (Distance < ClosestSelectionDistance) then begin
- ClosestSelection := AMemo.SelectionCount-1;
- ClosestSelectionDistance := Distance;
- end;
- StartPos := FoundRange.EndPos;
- end;
- if ClosestSelection <> -1 then begin
- AMemo.MainSelection := ClosestSelection;
- AMemo.ScrollCaretIntoView;
- end;
- end;
- end.
|