diff --git a/BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf b/BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf index f334ece6..75576b51 100644 --- a/BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf +++ b/BeefLibs/Beefy2D/src/theme/dark/DarkEditWidget.bf @@ -243,9 +243,9 @@ namespace Beefy.theme.dark return mLineCoords[anchorLine + 1] == mLineCoords[checkLine + 1]; } - protected override void AdjustCursorsAfterExternalEdit(int index, int ofs) + protected override void AdjustCursorsAfterExternalEdit(int index, int ofs, int lineOfs) { - base.AdjustCursorsAfterExternalEdit(index, ofs); + base.AdjustCursorsAfterExternalEdit(index, ofs, lineOfs); mWantsCheckScrollPosition = true; } @@ -775,6 +775,96 @@ namespace Beefy.theme.dark } } + if (mEditWidget.mHasFocus) + { + void DrawSelection(int line, int startColumn, int endColumn) + { + float x = startColumn * mCharWidth; + float y = mLineCoords[line]; + float width = Math.Abs(startColumn - endColumn) * mCharWidth; + float height = mLineCoords[line + 1] - y; + + using (g.PushColor(mHiliteColor)) + g.FillRect(x, y, width, height); + } + + void DrawSelection() + { + if (!HasSelection()) + return; + + mSelection.Value.GetAsForwardSelect(var startPos, var endPos); + GetLineColumnAtIdx(startPos, var startLine, var startColumn); + GetLineColumnAtIdx(endPos, var endLine, var endColumn); + + // Selection is on the single line + if (startLine == endLine) + { + DrawSelection(startLine, startColumn, endColumn); + return; + } + + // Selection goes across multiple lines + + // First line + GetLinePosition(startLine, ?, var firstLineEndIdx); + GetLineColumnAtIdx(firstLineEndIdx, ?, var firstLineEndColumn); + DrawSelection(startLine, startColumn, firstLineEndColumn + 1); + + for (var lineIdx = startLine + 1; lineIdx < endLine; lineIdx++) + { + GetLinePosition(lineIdx, var lineStart, var lineEnd); + GetLineColumnAtIdx(lineEnd, var line, var column); + + if (column == 0) + { + // Blank line selected + var y = mLineCoords[line]; + var height = mLineCoords[line + 1] - y; + + using (g.PushColor(mHiliteColor)) + g.FillRect(0, y, 4, height); + } + else + { + DrawSelection(line, 0, column + 1); + } + } + + // Last line + DrawSelection(endLine, 0, endColumn); + } + + var prevTextCursor = mCurrentTextCursor; + for (var cursor in mTextCursors) + { + if (cursor.mId == 0) + continue; + + SetTextCursor(cursor); + DrawSelection(); + + float x = 0; + float y = 0; + if (cursor.mVirtualCursorPos.HasValue) + { + x = cursor.mVirtualCursorPos.Value.mColumn * mCharWidth; + y = mLineCoords[cursor.mVirtualCursorPos.Value.mLine]; + } + else + { + GetLineCharAtIdx(cursor.mCursorTextPos, var eStartLine, var eStartCharIdx); + GetColumnAtLineChar(eStartLine, eStartCharIdx, var column); + x = column * mCharWidth; + y = mLineCoords[eStartLine]; + } + + using (g.PushColor(0xFF80FFB3)) + DrawCursor(x, y); + } + SetTextCursor(prevTextCursor); + } + g.PopMatrix(); /*using (g.PushColor(0x4000FF00)) diff --git a/BeefLibs/Beefy2D/src/utils/UndoManager.bf b/BeefLibs/Beefy2D/src/utils/UndoManager.bf index 22e894e5..d36f9460 100644 --- a/BeefLibs/Beefy2D/src/utils/UndoManager.bf +++ b/BeefLibs/Beefy2D/src/utils/UndoManager.bf @@ -138,8 +138,47 @@ namespace Beefy.utils mCurCost = 0; } + bool TryMerge(UndoAction action) + { + var currentBatchEnd = action as UndoBatchEnd; + if (currentBatchEnd == null) + return false; + + var currentBatchStart = currentBatchEnd.mBatchStart as UndoBatchStart; + var prevBatchEndIdx = mUndoList.IndexOf(currentBatchStart) - 1; + if (prevBatchEndIdx <= 0) + return false; + + var prevBatchEnd = mUndoList[prevBatchEndIdx] as UndoBatchEnd; + if (prevBatchEnd == null) + return false; + + var prevBatchStart = prevBatchEnd.mBatchStart as UndoBatchStart; + if (prevBatchStart == null) + return false; + if (prevBatchStart.Merge(currentBatchStart) == false) + return false; + + mUndoList.Remove(currentBatchStart); + mUndoList.Remove(currentBatchEnd); + + mUndoList.Remove(prevBatchEnd); + mUndoList.Add(prevBatchEnd); + + delete currentBatchStart; + delete currentBatchEnd; + + mUndoIdx = (.)mUndoList.Count; + + Debug.WriteLine("SUCCESS: Merged"); + return true; + } + public void Add(UndoAction action, bool allowMerge = true) { + if ((allowMerge) && (TryMerge(action))) + return; + if (mFreezeDeletes == 0) mCurCost += action.GetCost(); if (action is IUndoBatchStart) diff --git a/BeefLibs/Beefy2D/src/widgets/EditWidget.bf b/BeefLibs/Beefy2D/src/widgets/EditWidget.bf index 4209b4b6..18e5d0e1 100644 --- a/BeefLibs/Beefy2D/src/widgets/EditWidget.bf +++ b/BeefLibs/Beefy2D/src/widgets/EditWidget.bf @@ -101,6 +101,42 @@ namespace Beefy.widgets public uint8 mDisplayFlags; } + public class TextCursor + { + static int32 mNextId = 0; + + public int32 mId; + + public EditSelection? mSelection; + public EditWidgetContent.LineAndColumn? mVirtualCursorPos; + public int32 mCursorTextPos; + + public bool mCursorImplicitlyMoved; + public bool mJustInsertedCharPair; + + public this(int32 id, TextCursor textCursor = null) + { + mId = id; + if (id == -1) + { + mNextId++; + if (mNextId == 0) + mNextId++; + mId = mNextId; + } + + if (textCursor != null) + { + mSelection = textCursor.mSelection; + mVirtualCursorPos = textCursor.mVirtualCursorPos; + mCursorTextPos = textCursor.mCursorTextPos; + + if ((mSelection.HasValue) && (mSelection.Value.Length == 0)) + mSelection = null; + } + } + } + public class TextAction : UndoAction { public EditWidgetContent.Data mEditWidgetContentData; @@ -111,6 +147,7 @@ namespace Beefy.widgets public int32 mPrevTextVersionId; public int32 mCursorTextPos; public LineAndColumn? mVirtualCursorPos; + public int32 mTextCursorId; // Return either the focused edit widget content, or the last one added public EditWidgetContent EditWidgetContent @@ -136,11 +173,15 @@ namespace Beefy.widgets mSelection = editWidget.mSelection; mCursorTextPos = (int32)editWidget.CursorTextPos; mVirtualCursorPos = editWidget.mVirtualCursorPos; + mTextCursorId = editWidget.mCurrentTextCursor.mId; + if ((editWidget.IsPrimaryTextCursor()) && (editWidget.mMultiCursorUndoBatch != null)) + editWidget.mMultiCursorUndoBatch.mPrimaryUndoAction = this; } public void SetPreviousState(bool force) { var editWidgetContent = EditWidgetContent; + editWidgetContent.SetTextCursor(mTextCursorId); mEditWidgetContentData.mCurTextVersionId = mPrevTextVersionId; if ((mRestoreSelectionOnUndo) || (force)) editWidgetContent.mSelection = mSelection; @@ -151,6 +192,41 @@ namespace Beefy.widgets if (mMoveCursor) editWidgetContent.EnsureCursorVisible(); } + + static public int32 CalculateOffset(EditWidgetContent ewc, StringView text) + { + // Calculates an offset which is used to determine, if two undo-actions + // from two MultiCursorUndoBatchStart can or cannot be "merged". + // It assumes, that every cursor will insert the same amout of text. + // NOTE: This can be used to actually merge two UndoAction, however, + // I (kallisto56) have not figured out how to properly undo/redo, because + // each consecutive action is made at different CursorTextPos, + // shifted by surrounding cursors. + if (ewc.mTextCursors.Count == 1) + return 0; + + var currentSelection = ewc.GetAsSelection(ewc.mCurrentTextCursor, true); + var offset = 0; + if (currentSelection.Length > 0) + offset -= ewc.mSelection.Value.Length; + + for (var cursor in ewc.mTextCursors) + { + if (cursor.mId == ewc.mCurrentTextCursor.mId) + continue; + + var otherSelection = ewc.GetAsSelection(cursor, true); + if (currentSelection.mStartPos > otherSelection.mStartPos) + { + if (otherSelection.Length > 0) + offset -= otherSelection.Length; + + offset += text.Length; + } + } + + return (int32)offset; + } } public class SetCursorAction : TextAction @@ -209,6 +285,79 @@ namespace Beefy.widgets } } + public class MultiCursorUndoBatchStart : UndoBatchStart + { + public List mTextCursorsIds = new List() ~ delete _; + public TextAction mPrimaryUndoAction = null; + public int32 mOffsetToNextUndoAction = int32.MinValue; + + public this(EditWidgetContent ewc, String name) + : base(name) + { + for (var cursor in ewc.mTextCursors) + mTextCursorsIds.Add(cursor.mId); + } + + public override bool Merge(UndoAction nextAction) + { + var undoBatchStart = nextAction as MultiCursorUndoBatchStart; + if (undoBatchStart == null) + return false; + if (mName != undoBatchStart.mName) + return false; + if (mTextCursorsIds.Count != undoBatchStart.mTextCursorsIds.Count) + return false; + + // Checking that sequence of ids for text cursors matches + for (var i = 0; i < mTextCursorsIds.Count; i++) + { + if (mTextCursorsIds[i] != undoBatchStart.mTextCursorsIds[i]) + return false; + } + + var lhsInsertText = mPrimaryUndoAction as InsertTextAction; + var rhsInsertText = undoBatchStart.mPrimaryUndoAction as InsertTextAction; + var lhsDeleteChar = mPrimaryUndoAction as DeleteCharAction; + var rhsDeleteChar = undoBatchStart.mPrimaryUndoAction as DeleteCharAction; + var lhsDeleteSelection = mPrimaryUndoAction as DeleteSelectionAction; + var rhsDeleteSelection = undoBatchStart.mPrimaryUndoAction as DeleteSelectionAction; + + if ((lhsInsertText != null) || (rhsInsertText != null)) + { + if ((lhsInsertText == null) || (rhsInsertText == null)) + return false; + + int curIdx = lhsInsertText.mCursorTextPos + mOffsetToNextUndoAction; + int nextIdx = rhsInsertText.mCursorTextPos; + if ((nextIdx != curIdx + lhsInsertText.mText.Length) || + (rhsInsertText.mText.EndsWith("\n")) || + (lhsInsertText.mText == "\n")) + return false; + } + else if ((lhsDeleteChar != null) || (rhsDeleteChar != null)) + { + if ((lhsDeleteChar == null) || (rhsDeleteChar == null)) + return false; + if ((rhsDeleteChar.[Friend]mOffset < 0) != (lhsDeleteChar.[Friend]mOffset < 0)) + return false; + + int32 curIdx = lhsDeleteChar.[Friend]mCursorTextPos - mOffsetToNextUndoAction; + int32 nextIdx = rhsDeleteChar.mCursorTextPos; + if (nextIdx != curIdx + lhsDeleteChar.[Friend]mOffset) + return false; + } + else if ((lhsDeleteSelection != null) || (rhsDeleteSelection != null)) + { + return false; + } + + mPrimaryUndoAction = undoBatchStart.mPrimaryUndoAction; + mOffsetToNextUndoAction = undoBatchStart.mOffsetToNextUndoAction; + + return true; + } + } + public class DeleteSelectionAction : TextAction { public String mSelectionText; @@ -221,6 +370,8 @@ namespace Beefy.widgets mSelectionText = new String(); editWidgetContent.GetSelectionText(mSelectionText); } + if ((editWidgetContent.IsPrimaryTextCursor()) && (editWidgetContent.mMultiCursorUndoBatch != null)) + editWidgetContent.mMultiCursorUndoBatch.mOffsetToNextUndoAction = CalculateOffset(editWidgetContent, mSelectionText); } ~this() @@ -231,6 +382,7 @@ namespace Beefy.widgets public override bool Undo() { var editWidgetContent = EditWidgetContent; + editWidgetContent.SetTextCursor(mTextCursorId); if (mSelection != null) { bool wantSnapScroll = mEditWidgetContentData.mTextLength == 0; @@ -314,6 +466,8 @@ namespace Beefy.widgets { mText = new String(text); mRestoreSelectionOnUndo = !insertFlags.HasFlag(.NoRestoreSelectionOnUndo); + if ((editWidget.IsPrimaryTextCursor()) && (editWidget.mMultiCursorUndoBatch != null)) + editWidget.mMultiCursorUndoBatch.mOffsetToNextUndoAction = CalculateOffset(editWidget, mText); } public override bool Merge(UndoAction nextAction) @@ -321,6 +475,8 @@ namespace Beefy.widgets InsertTextAction insertTextAction = nextAction as InsertTextAction; if (insertTextAction == null) return false; + if (mTextCursorId != insertTextAction.mTextCursorId) + return false; int curIdx = mCursorTextPos; int nextIdx = insertTextAction.mCursorTextPos; @@ -357,6 +513,7 @@ namespace Beefy.widgets Debug.Assert(mSelection.Value.HasSelection); var editWidgetContent = EditWidgetContent; + editWidgetContent.SetTextCursor(mTextCursorId); int startIdx = (mSelection != null) ? mSelection.Value.MinPos : mCursorTextPos; editWidgetContent.RemoveText(startIdx, (int32)mText.Length); editWidgetContent.ContentChanged(); @@ -417,6 +574,8 @@ namespace Beefy.widgets int32 textPos = mCursorTextPos; mText = new String(count); editWidgetContent.ExtractString(textPos + offset, count, mText); + if ((editWidget.IsPrimaryTextCursor()) && (editWidget.mMultiCursorUndoBatch != null)) + editWidget.mMultiCursorUndoBatch.mOffsetToNextUndoAction = CalculateOffset(editWidget, mText); } public override bool Merge(UndoAction nextAction) @@ -426,6 +585,8 @@ namespace Beefy.widgets return false; if ((deleteCharAction.mOffset < 0) != (mOffset < 0)) return false; + if (mTextCursorId != deleteCharAction.mTextCursorId) + return false; int32 curIdx = mCursorTextPos; int32 nextIdx = deleteCharAction.mCursorTextPos; @@ -451,6 +612,7 @@ namespace Beefy.widgets { int32 textPos = mCursorTextPos; var editWidgetContent = EditWidgetContent; + editWidgetContent.SetTextCursor(mTextCursorId); editWidgetContent.CursorTextPos = textPos + mOffset; @@ -579,10 +741,13 @@ namespace Beefy.widgets public uint8 mExtendDisplayFlags; public uint8 mInsertDisplayFlags; + public MultiCursorUndoBatchStart mMultiCursorUndoBatch; + public List mTextCursors = new List() ~ DeleteContainerAndItems!(_); + public TextCursor mCurrentTextCursor; public int32 mCursorBlinkTicks; - public bool mCursorImplicitlyMoved; - public bool mJustInsertedCharPair; // Pressing backspace will delete last char8, even though cursor is between char8 pairs (ie: for brace pairs 'speculatively' inserted) - public EditSelection? mSelection; + public ref bool mCursorImplicitlyMoved => ref mCurrentTextCursor.mCursorImplicitlyMoved; + public ref bool mJustInsertedCharPair => ref mCurrentTextCursor.mJustInsertedCharPair; // Pressing backspace will delete last char8, even though cursor is between char8 pairs (ie: for brace pairs 'speculatively' inserted) + public ref EditSelection? mSelection => ref mCurrentTextCursor.mSelection; public EditSelection? mDragSelectionUnion; // For double-clicking a word and then "dragging" the selection public DragSelectionKind mDragSelectionKind; public bool mIsReadOnly = false; @@ -592,9 +757,9 @@ namespace Beefy.widgets public float mCursorWantX; // For keyboard cursor selection, accounting for when we truncate to line end public bool mOverTypeMode = false; - public int32 mCursorTextPos; + public ref int32 mCursorTextPos => ref mCurrentTextCursor.mCursorTextPos; public bool mShowCursorAtLineEnd; - public LineAndColumn? mVirtualCursorPos; + public ref LineAndColumn? mVirtualCursorPos => ref mCurrentTextCursor.mVirtualCursorPos; public bool mEnsureCursorVisibleOnModify = true; public bool mAllowVirtualCursor; public bool mAllowMaximalScroll = true; // Allows us to scroll down such that edit widget is blank except for one line of content at the top @@ -686,6 +851,7 @@ namespace Beefy.widgets mData = CreateEditData(); mData.Ref(this); mContentChanged = true; + mCurrentTextCursor = mTextCursors.Add(.. new TextCursor(0)); } @@ -790,6 +956,16 @@ namespace Beefy.widgets public override void MouseDown(float x, float y, int32 btn, int32 btnCount) { + SetPrimaryTextCursor(); + if ((mIsMultiline) && (btn == 0) && (mWidgetWindow.GetKeyFlags(true) == .Alt)) + { + mTextCursors.Add(new TextCursor(-1, mCurrentTextCursor)); + } + else + { + RemoveSecondaryTextCursors(); + } + base.MouseDown(x, y, btn, btnCount); mEditWidget.SetFocus(); @@ -997,20 +1173,64 @@ namespace Beefy.widgets #endif } - protected virtual void AdjustCursorsAfterExternalEdit(int index, int ofs) + protected virtual void AdjustCursorsAfterExternalEdit(int index, int ofs, int lineOfs) { -#unwarn - int cursorPos = CursorTextPos; - if (cursorPos >= index) - CursorTextPos = Math.Clamp(mCursorTextPos + (int32)ofs, 0, mData.mTextLength + 1); - if (HasSelection()) - { - if (((ofs > 0) && (mSelection.Value.mStartPos >= index)) || - ((ofs < 0) && (mSelection.Value.mStartPos > index))) - mSelection.ValueRef.mStartPos += (int32)ofs; - if (mSelection.Value.mEndPos > index) - mSelection.ValueRef.mEndPos += (int32)ofs; + var prevTextCursor = mCurrentTextCursor; + + for (var cursor in mTextCursors) + { + if ((cursor.mId == prevTextCursor.mId) && (mEditWidget.mHasFocus)) + continue; + + SetTextCursor(cursor); + + if (mEditWidget.mHasFocus) + { + if (HasSelection()) + { + var isCaretAtStartPos = (mSelection.Value.mStartPos == mCursorTextPos); + + if (((ofs > 0) && (mSelection.Value.MinPos >= index)) || + ((ofs < 0) && (mSelection.Value.MinPos > index))) + { + mSelection.ValueRef.mStartPos = Math.Clamp(mSelection.Value.mStartPos + int32(ofs), 0, mData.mTextLength + 1); + mSelection.ValueRef.mEndPos = Math.Clamp(mSelection.Value.mEndPos + int32(ofs), 0, mData.mTextLength + 1); + } + + mCursorTextPos = (isCaretAtStartPos) + ? mSelection.Value.mStartPos + : mSelection.Value.mEndPos; + } + else + if (((ofs > 0) && (CursorTextPos > index)) || + ((ofs < 0) && (CursorTextPos >= index - ofs))) + { + mCursorTextPos = Math.Clamp(mCursorTextPos + int32(ofs), 0, mData.mTextLength + 1); + + if (mVirtualCursorPos.HasValue) + mVirtualCursorPos = LineAndColumn(mVirtualCursorPos.Value.mLine + lineOfs, mVirtualCursorPos.Value.mColumn); + } + + if (mSelection.HasValue && mSelection.Value.mStartPos == mSelection.Value.mEndPos) + mSelection = null; + } + else + { + int cursorPos = CursorTextPos; + if (cursorPos >= index) + CursorTextPos = Math.Clamp(mCursorTextPos + (int32)ofs, 0, mData.mTextLength + 1); + if (HasSelection()) + { + if (((ofs > 0) && (mSelection.Value.mStartPos >= index)) || + ((ofs < 0) && (mSelection.Value.mStartPos > index))) + mSelection.ValueRef.mStartPos += (int32)ofs; + if (mSelection.Value.mEndPos > index) + mSelection.ValueRef.mEndPos += (int32)ofs; + } + } } + + SetTextCursor(prevTextCursor); } public virtual void ApplyTextFlags(int index, String text, uint8 typeNum, uint8 flags) @@ -1070,24 +1290,53 @@ namespace Beefy.widgets VerifyTextIds(); + var lineOfs = 0; + for (var n = 0; n < text.Length; n++) + { + if ((text[n] == '\n') || (text[n] == '\r')) + lineOfs++; + } + for (var user in mData.mUsers) { if (user != this) { - user.AdjustCursorsAfterExternalEdit(index, (int32)text.Length); + user.AdjustCursorsAfterExternalEdit(index, (int32)text.Length, lineOfs); user.ContentChanged(); } } + + AdjustCursorsAfterExternalEdit(index, text.Length, lineOfs); } public virtual void RemoveText(int index, int length) { + var lineOfs = 0; + if (length != 0) + { + var startPos = index; + var endPos = index + length; + if (startPos > endPos) + Swap!(startPos, endPos); + + endPos = Math.Clamp(endPos, 0, mData.mTextLength); + + for (var n = startPos; n < endPos; n++) + { + var char = mData.mText[n].mChar; + if ((char == '\n') || (char == '\r')) + { + lineOfs++; + } + } + } + for (var user in mData.mUsers) { if (user != this) { - user.AdjustCursorsAfterExternalEdit(index, -length); + user.AdjustCursorsAfterExternalEdit(index, -length, -lineOfs); user.ContentChanged(); } } @@ -1102,6 +1351,7 @@ namespace Beefy.widgets TextChanged(); VerifyTextIds(); + AdjustCursorsAfterExternalEdit(index, -length, -lineOfs); } /*public void PhysInsertAtCursor(String theString, bool moveCursor = true) @@ -1769,6 +2019,7 @@ namespace Beefy.widgets { if (!CheckReadOnly()) { + CreateMultiCursorUndoBatch("EWC.KeyChar(Ctrl+Backspace)"); int line; int lineChar; GetCursorLineChar(out line, out lineChar); @@ -1795,6 +2046,7 @@ namespace Beefy.widgets { if (!CheckReadOnly()) { + CreateMultiCursorUndoBatch("EWC.KeyChar(\b)"); if (HasSelection()) { DeleteSelection(); @@ -1877,7 +2129,10 @@ namespace Beefy.widgets break; case '\t': if (AllowChar(useChar)) + { + CreateMultiCursorUndoBatch("EWC.KeyChar(\t)"); BlockIndentSelection(mWidgetWindow.IsKeyDown(KeyCode.Shift)); + } return; } @@ -1893,6 +2148,7 @@ namespace Beefy.widgets if ((AllowChar(useChar)) && (!CheckReadOnly())) { + CreateMultiCursorUndoBatch("EWC.KeyChar"); if ((useChar == '\n') && (!mAllowVirtualCursor)) { int lineIdx; @@ -2080,32 +2336,184 @@ namespace Beefy.widgets void CopyText(bool cut) { - bool selectedLine = false; - String extra = scope .(); - if (!HasSelection()) - { - selectedLine = true; - GetLinePosition(CursorLineAndColumn.mLine, var lineStart, var lineEnd); - mSelection = .(lineStart, lineEnd); - extra.Append("line"); - } - - String selText = scope String(); - GetSelectionText(selText); - BFApp.sApp.SetClipboardText(selText, extra); + if (!IsPrimaryTextCursor()) + return; + if ((cut) && (!CheckReadOnly())) - { - if (selectedLine) - { - // Remove \n - if (mSelection.Value.mEndPos < mData.mTextLength) - mSelection.ValueRef.mEndPos++; - } - DeleteSelection(); + { + // Forcing creation of undo batch, because even single cursor has + // multiple undo-actions (SetCursorAction + DeleteSelectionAction). + CreateMultiCursorUndoBatch("EWC.CopyText(cut=true)", force: true); } - if (selectedLine) + var text = scope String(); + var extra = scope String(); + + // List of cursors, ordered from the first one in the document, to the last. + var sortedCursors = GetSortedCursors(.. scope List()); + + // Copy stage (iterate in order from first to last in the document) + for (var cursor in sortedCursors) + { + SetTextCursor(cursor); + + var cursorText = scope String(); + var cursorExtra = String.Empty; + var selectedLine = false; + EditSelection selection; + + if (!HasSelection()) + { + cursorExtra = "line"; + selectedLine = true; + GetLinePosition(CursorLineAndColumn.mLine, var lineStart, var lineEnd); + selection = EditSelection(lineStart, lineEnd); + } + else + { + mSelection = selection = GetAsSelection(cursor, true); + } + + // ... + ExtractString(selection.mStartPos, selection.Length, cursorText); + cursorText.Append('\n'); + + text.Append(cursorText); + extra.AppendF("{0}:{1};", cursorExtra, cursorText.Length); + } + + BFApp.sApp.SetClipboardText(text, extra); + SetPrimaryTextCursor(); + + if ((!cut) || (CheckReadOnly())) + return; + + int32 GetLine(TextCursor cursor) + { + if (cursor.mVirtualCursorPos.HasValue) + return cursor.mVirtualCursorPos.Value.mLine; + + int line; + int lineChar; + GetLineCharAtIdx(cursor.mCursorTextPos, out line, out lineChar); + + int coordLineColumn; + GetLineAndColumnAtLineChar(line, lineChar, out coordLineColumn); + return (int32)line; + } + + // Cursors that have selection + var selections = scope List<(int32 mLine, TextCursor mCursor)>(); + for (var idx = 0; idx < sortedCursors.Count; idx++) + { + var cursor = sortedCursors[idx]; + SetTextCursor(cursor); + + var undoAction = mData.mUndoManager.Add(.. new SetCursorAction(this)); + undoAction.mRestoreSelectionOnUndo = true; + + if (HasSelection()) + { + DeleteSelection(); + sortedCursors.RemoveAt(idx); + selections.Add((.)(GetLine(cursor), cursor)); + idx--; + } + } + + TextCursor GetTextCursor(int32 excludeId, int32 line) + { + for (var cursor in sortedCursors) + { + if (cursor.mId == excludeId) + continue; + + if (GetLine(cursor) == line) + return cursor; + } + + return null; + } + + // At this point we have a list of carets and a list of selections + // Selection, that sits on the same line with a caret should be deleted + // Multiple carets on the same line should be deleted leaving only one of them + for (var idx = sortedCursors.Count - 1; idx >= 0; idx--) + { + if (idx > sortedCursors.Count-1) + idx = sortedCursors.Count-1; + if (sortedCursors.Count == 0) + break; + + var cursor = sortedCursors[idx]; + var cursorLine = GetLine(cursor); + + SetTextCursor(cursor); + + // Go over selections and delete those, that sit on the same line with a this caret + for (var sidx = selections.Count - 1; sidx >= 0; sidx--) + { + var selection = selections[sidx]; + + if (selection.mLine == cursorLine) + { + if (selection.mCursor.mId == 0) + { + // +1 because we're going to delete that line and we want that primary selection to stay on the same line + selection.mCursor.mVirtualCursorPos = LineAndColumn(cursorLine+1, 0); + selections.RemoveAt(sidx); + continue; + } + + mTextCursors.Remove(selection.mCursor); + selections.RemoveAt(sidx); + delete selection.mCursor; + } + } + + // Get another caret that sits on the same line + while (true) + { + if (cursor == null) + break; + var anotherCursor = GetTextCursor(cursor.mId, cursorLine); + if (anotherCursor == null) + break; + + if (anotherCursor.mId == 0) + { + mTextCursors.Remove(cursor); + sortedCursors.Remove(cursor); + delete cursor; + cursor = null; + } + else + { + mTextCursors.Remove(anotherCursor); + sortedCursors.Remove(anotherCursor); + delete anotherCursor; + } + } + + if (cursor == null) + { + idx = sortedCursors.Count; + continue; + } + + GetLinePosition(CursorLineAndColumn.mLine, var lineStart, var lineEnd); + mSelection = .(lineStart, lineEnd); + if (mSelection.Value.mEndPos < mData.mTextLength) + mSelection.ValueRef.mEndPos++; + DeleteSelection(); mSelection = null; + + sortedCursors.Remove(cursor); + if (sortedCursors.Count == 0) + break; + } + + SetPrimaryTextCursor(); } public void CutText() @@ -2120,45 +2528,148 @@ namespace Beefy.widgets public void PasteText(String text, String extra) { - if ((extra == "line") && (mAllowVirtualCursor)) + if (!IsPrimaryTextCursor()) + return; + if (CheckReadOnly()) + return; + + CreateMultiCursorUndoBatch("EWC.PasteText(text, extra)"); + + // Decode 'extra' into fragments + // Support for older builds is accounted for later on in this method + var offset = 0; + var fragments = scope List<(StringView mText, StringView mExtra)>(); + var enumerator = extra.Split(':', ';'); + while (enumerator.MoveNext()) { - UndoBatchStart undoBatchStart = new UndoBatchStart("paste"); - mData.mUndoManager.Add(undoBatchStart); + var cursorExtra = enumerator.Current; + if (enumerator.GetNext() case .Err) + break; - var setCursorAction = new SetCursorAction(this); - mData.mUndoManager.Add(setCursorAction); + var length = int.Parse(enumerator.Current); + var cursorText = StringView(text, offset, length); + fragments.Add((cursorText, cursorExtra)); + offset += length; + } - var origLineAndColumn = CursorLineAndColumn; - CursorLineAndColumn = .(origLineAndColumn.mLine, 0); - var lineStartPosition = CursorLineAndColumn; - InsertAtCursor("\n"); - CursorLineAndColumn = lineStartPosition; - - CursorToLineStart(false); - - // Adjust to requested column - if (CursorLineAndColumn.mColumn != 0) + void PasteFragment(String cursorText, StringView cursorExtra) + { + if ((cursorExtra.Length == 0) || (HasSelection())) { - for (let c in text.RawChars) + /*if ((cursorExtra.Length == 0) && (cursorText[cursorText.Length-1] == '\n')) + cursorText.RemoveFromEnd(1);*/ + PasteText(cursorText); + } + else// if (fragment.mExtra == "line") + { + // Case, when clipboard fragment is a line and cursor is a caret + var origLineAndColumn = CursorLineAndColumn; + CursorLineAndColumn = .(origLineAndColumn.mLine, 0); + var lineStartPosition = CursorLineAndColumn; + InsertAtCursor("\n"); + CursorLineAndColumn = lineStartPosition; + + CursorToLineStart(false); + + // Adjust to requested column + if (CursorLineAndColumn.mColumn != 0) { - if (!c.IsWhiteSpace) + for (let c in cursorText.RawChars) { - text.Remove(0, @c.Index); - break; + if (!c.IsWhiteSpace) + { + cursorText.Remove(0, @c.Index); + break; + } } } + + PasteText(cursorText); + CursorLineAndColumn = .(origLineAndColumn.mLine + 1, origLineAndColumn.mColumn); + } + } + + // When clipboard contains text with multiple cursors, but we only have one, + // We create cursor for each fragment and make it so that the primary cursor + // is the last one in the document. + if ((mTextCursors.Count == 1) && (fragments.Count > 1)) + { + // By default, undo-batch is only created when we have multiple cursors + CreateMultiCursorUndoBatch("EWC.PasteText(text, extra)", force: true); + SetPrimaryTextCursor(); + + for (var idx = 0; idx < fragments.Count; idx++) + { + var fragment = fragments[idx]; + var length = fragment.mText.Length; + if (idx + 1 == fragments.Count) + length--; + + PasteFragment(scope String(fragment.mText, 0, length), ""); + if (idx + 1 < fragments.Count) + { + var secondary = mTextCursors.Add(.. new TextCursor(-1, mCurrentTextCursor)); + secondary.mSelection = null; + if (secondary.mCursorTextPos > 0) + secondary.mCursorTextPos--; + } + else if ((mCursorTextPos > 0) && (idx + 1 != fragments.Count)) + { + CursorTextPos--; + } + } + return; + } + + // Case, when we have multiple cursors + var identicalCountOfCusrors = ((mTextCursors.Count == fragments.Count) || (mTextCursors.Count == 1 && fragments.Count == 0)); + var sortedCursors = GetSortedCursors(.. scope List()); + var idx = sortedCursors.Count-1; + + // This is for cases, when we have a single cursor. + UndoBatchStart undoBatchStart = null; + if (sortedCursors.Count == 1) + undoBatchStart = mData.mUndoManager.Add(.. new UndoBatchStart("paste")); + + if (!identicalCountOfCusrors) + { + text.RemoveFromEnd(1); + } + + for (var cursor in sortedCursors.Reversed) + { + SetTextCursor(cursor); + + if (fragments.Count == 0) + { + PasteFragment(text, extra); + continue; + } + else if (!identicalCountOfCusrors) + { + PasteFragment(text, fragments[0].mExtra); + continue; } - PasteText(text); - CursorLineAndColumn = .(origLineAndColumn.mLine + 1, origLineAndColumn.mColumn); - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); + var fragment = fragments[idx--]; + String cursorText = scope String(fragment.mText, 0, fragment.mText.Length); + cursorText.RemoveFromEnd(1); + + mData.mUndoManager.Add(new SetCursorAction(this)); + PasteFragment(cursorText, fragment.mExtra); } - else - PasteText(text); + + SetPrimaryTextCursor(); + + if (undoBatchStart != null) + mData.mUndoManager.Add(undoBatchStart.mBatchEnd); } public void PasteText() { + if (!IsPrimaryTextCursor()) + return; + String aText = scope String(); String extra = scope .(); BFApp.sApp.GetClipboardText(aText, extra); @@ -2620,6 +3131,7 @@ namespace Beefy.widgets { if (!CheckReadOnly()) { + CreateMultiCursorUndoBatch("EWC.KeyDown(Ctrl/Shift+Delete)"); if (mWidgetWindow.IsKeyDown(.Shift)) { int startIdx = CursorTextPos; @@ -2658,6 +3170,7 @@ namespace Beefy.widgets if (!CheckReadOnly()) { + CreateMultiCursorUndoBatch("EWC.KeyDown(DeleteChar)"); DeleteChar(); } mCursorImplicitlyMoved = true; @@ -3177,6 +3690,8 @@ namespace Beefy.widgets public virtual void EnsureCursorVisible(bool scrollView = true, bool centerView = false, bool doHorzJump = true) { + if (!IsPrimaryTextCursor()) + return; if (mEditWidget.mScrollContentContainer.mWidth <= 0) return; // Not sized yet @@ -3663,6 +4178,282 @@ namespace Beefy.widgets } return false; } + + public mixin SetPrimaryTextCursorScoped() + { + if (mCurrentTextCursor.mId != 0) + { + var previousTextCursor = mCurrentTextCursor; + SetTextCursor(mTextCursors.Front); + defer :: SetTextCursor(previousTextCursor); + } + } + + [Inline] + public void SetTextCursor(TextCursor cursor) + { + Debug.Assert(cursor != null); + mCurrentTextCursor = cursor; + } + + [Inline] + public bool IsPrimaryTextCursor() + { + return (mCurrentTextCursor.mId == 0); + } + + public void SetTextCursor(int32 id) + { + if (id == 0) + { + mCurrentTextCursor = mTextCursors.Front; + return; + } + + for (var cursor in mTextCursors) + { + if (cursor.mId == id) + { + mCurrentTextCursor = cursor; + return; + } + } + + var cursor = mTextCursors.Add(.. new TextCursor(id)); + mCurrentTextCursor = cursor; + } + + [Inline] + public void SetPrimaryTextCursor() + { + Debug.Assert((mTextCursors.Count > 0) && (mTextCursors.Front.mId == 0)); + mCurrentTextCursor = mTextCursors.Front; + } + + public EditSelection GetAsSelection(TextCursor cursor, bool getAsForwardSelection) + { + if (cursor.mSelection.HasValue) + { + if (getAsForwardSelection && cursor.mSelection.Value.IsForwardSelect == false) + { + cursor.mSelection.Value.GetAsForwardSelect(var start, var end); + return EditSelection(start, end); + } + + return cursor.mSelection.Value; + } + else if (cursor.mVirtualCursorPos.HasValue) + { + var line = cursor.mVirtualCursorPos.Value.mLine; + var column = cursor.mVirtualCursorPos.Value.mColumn; + + GetTextCoordAtLineAndColumn(line, column, var x, var y); + GetLineCharAtCoord(line, x, var lineChar, ?); + int textPos = GetTextIdx(line, lineChar); + + return EditSelection(textPos, textPos); + } + else + { + return EditSelection(cursor.mCursorTextPos, cursor.mCursorTextPos); + } + } + + public void RemoveSecondaryTextCursors() + { + if (mTextCursors.Count == 1) + return; + + for (var idx = 1; idx < mTextCursors.Count; idx++) + delete mTextCursors[idx]; + + mTextCursors.Resize(1); + mCurrentTextCursor = mTextCursors.Front; + } + + public void CreateMultiCursorUndoBatch(String name, bool force = false) + { + if ((mMultiCursorUndoBatch == null) && ((mTextCursors.Count > 1) || (force))) + { + mMultiCursorUndoBatch = new MultiCursorUndoBatchStart(this, name); + mData.mUndoManager.Add(mMultiCursorUndoBatch); + } + } + + public void CloseMultiCursorUndoBatch() + { + if (mMultiCursorUndoBatch == null) + return; + + mData.mUndoManager.Add(mMultiCursorUndoBatch.mBatchEnd); + mMultiCursorUndoBatch = null; + } + + public void GetSortedCursors(List output) + { + output.AddRange(mTextCursors); + output.Sort(scope => CompareTextCursors); + } + + int CompareTextCursors(TextCursor lhs, TextCursor rhs) + { + var lhsSelection = GetAsSelection(lhs, true); + var rhsSelection = GetAsSelection(rhs, true); + return (lhsSelection.mStartPos <=> rhsSelection.mStartPos); + } + + public void RemoveIntersectingTextCursors() + { + if (mTextCursors.Count == 1) + return; + + for (var x = mTextCursors.Count-1; x >= 0; x--) + { + for (var y = mTextCursors.Count-1; y >= 0; y--) + { + if (x == y) + continue; + + var lhs = mTextCursors[x]; + var rhs = mTextCursors[y]; + + if (TextCursorsIntersects(lhs, rhs)) + { + if (lhs.mId != 0) + { + delete mTextCursors[x]; + mTextCursors.RemoveAt(x); + } + else + { + delete mTextCursors[y]; + mTextCursors.RemoveAt(y); + } + + break; + } + } + } + } + + public bool TextCursorsIntersects(TextCursor lhs, TextCursor rhs) + { + // Returns true if two text cursors intersect or collide with each other + var lhsSelection = GetAsSelection(lhs, true); + var rhsSelection = GetAsSelection(rhs, true); + + if (lhsSelection.mStartPos == rhsSelection.mStartPos) + return true; + + return !((lhsSelection.mEndPos <= rhsSelection.mStartPos) || (rhsSelection.mEndPos <= lhsSelection.mStartPos)); + } + + public void SelectNextMatch(bool createCursor = true, bool exhaustiveSearch = false) + { + SetPrimaryTextCursor(); + + if (!HasSelection()) + return; + + mJustInsertedCharPair = false; + mCursorImplicitlyMoved = false; + + var text = scope String(); + ExtractString(mSelection.Value.MinPos, mSelection.Value.Length, text); + + bool Matches(int startPos) + { + if (startPos + text.Length >= mData.mTextLength) + return false; + + for (var idx = 0; idx < text.Length; idx++) + { + var char = mData.mText[idx + startPos].mChar; + if (char != text[idx]) + return false; + } + + return true; + } + + bool IsSelectionExists(int startPos, int endPos, out TextCursor textCursor) + { + textCursor = null; + + for (var cursor in mTextCursors) + { + if (!cursor.mSelection.HasValue) + continue; + if ((cursor.mSelection.Value.MinPos == startPos) && (cursor.mSelection.Value.MaxPos == endPos)) + { + textCursor = cursor; + return true; + } + } + + return false; + } + + var startPos = mSelection.Value.MaxPos; + var endPos = (int)mData.mTextLength; + var found = false; + + while (true) + { + for (var idx = startPos; idx < endPos; idx++) + { + if (!Matches(idx)) + continue; + + if (IsSelectionExists(idx, idx + text.Length, var cursor)) + { + Swap!(mSelection, cursor.mSelection); + Swap!(mCursorTextPos, cursor.mCursorTextPos); + EnsureCursorVisible(); + + if (!createCursor) + { + mTextCursors.Remove(cursor); + delete cursor; + } + + return; + } + + if (createCursor) + mTextCursors.Add(new TextCursor(-1, mCurrentTextCursor)); + + // Making selection consistent across all cursors + mSelection = (mSelection.Value.IsForwardSelect) + ? EditSelection(idx, idx+text.Length) + : EditSelection(idx+text.Length, idx); + mCursorTextPos = mSelection.Value.mEndPos; + mVirtualCursorPos = null; + + if (!exhaustiveSearch) + { + EnsureCursorVisible(); + return; + } + } + + // Exit while-loop, if we already tried searching from the start + if ((!found) && (startPos == 0)) + return; + + // From initial 'startPos' to mData.mTextLength no match has been found + if (!found) + { + endPos = (startPos - text.Length); + startPos = 0; + } + } + } + + public void SkipCurrentMatchAndSelectNext() + { + SelectNextMatch(createCursor: false); + } } public abstract class EditWidget : ScrollableWidget @@ -3851,5 +4642,50 @@ namespace Beefy.widgets mVertPos.mPct = 1.0f; UpdateContentPosition(); } + + public override void KeyDown(KeyDownEvent keyEvent) + { + var ewc = Content; + var isSingleInvoke = false; + var keyFlags = mWidgetWindow.GetKeyFlags(true); + + if (((keyEvent.mKeyCode == (.)'Z') || (keyEvent.mKeyCode == (.)'Y')) && (keyFlags.HasFlag(.Ctrl))) + { + ewc.RemoveSecondaryTextCursors(); + isSingleInvoke = true; + } + else if (keyEvent.mKeyCode == .Escape) + { + ewc.RemoveSecondaryTextCursors(); + isSingleInvoke = true; + } + + ewc.RemoveIntersectingTextCursors(); + + for (var cursor in ewc.mTextCursors) + { + ewc.SetTextCursor(cursor); + base.KeyDown(keyEvent); + if (isSingleInvoke) + break; + } + + ewc.SetPrimaryTextCursor(); + ewc.CloseMultiCursorUndoBatch(); + } + + public override void KeyChar(KeyCharEvent keyEvent) + { + var ewc = Content; + ewc.RemoveIntersectingTextCursors(); + for (var cursor in ewc.mTextCursors) + { + ewc.SetTextCursor(cursor); + base.KeyChar(keyEvent); + } + + ewc.SetPrimaryTextCursor(); + ewc.CloseMultiCursorUndoBatch(); + } } } \ No newline at end of file diff --git a/IDE/src/Commands.bf b/IDE/src/Commands.bf index 7a51bcd3..75683b03 100644 --- a/IDE/src/Commands.bf +++ b/IDE/src/Commands.bf @@ -340,6 +340,8 @@ namespace IDE Add("Zoom Out", new => gApp.Cmd_ZoomOut); Add("Zoom Reset", new => gApp.Cmd_ZoomReset); Add("Attach to Process", new => gApp.[Friend]DoAttach); + Add("Select Next Match", new => gApp.Cmd_SelectNextMatch); + Add("Skip Current Match and Select Next", new => gApp.Cmd_SkipCurrentMatchAndSelectNext); Add("Test Enable Console", new => gApp.Cmd_TestEnableConsole); } diff --git a/IDE/src/IDEApp.bf b/IDE/src/IDEApp.bf index 1b03083d..e0a36ed7 100644 --- a/IDE/src/IDEApp.bf +++ b/IDE/src/IDEApp.bf @@ -4290,6 +4290,7 @@ namespace IDE var sourceViewPanel = GetActiveSourceViewPanel(); if (sourceViewPanel != null) { + sourceViewPanel.EditWidget.Content.RemoveSecondaryTextCursors(); sourceViewPanel.GotoLine(); return; } @@ -5469,7 +5470,7 @@ namespace IDE if (ewc.HasSelection()) ewc.GetSelectionText(debugExpr); else - sourceViewPanel.GetDebugExpressionAt(ewc.CursorTextPos, debugExpr); + sourceViewPanel.GetDebugExpressionAt(ewc.mTextCursors.Front.mCursorTextPos, debugExpr); dialog.Init(debugExpr); } else if (let immediatePanel = activePanel as ImmediatePanel) @@ -5951,6 +5952,18 @@ namespace IDE ToggleCheck(ideCommand.mMenuItem, ref mTestEnableConsole); } + [IDECommand] + public void Cmd_SelectNextMatch() + { + GetActiveSourceEditWidgetContent()?.SelectNextMatch(); + } + + [IDECommand] + public void Cmd_SkipCurrentMatchAndSelectNext() + { + GetActiveSourceEditWidgetContent()?.SkipCurrentMatchAndSelectNext(); + } + public void UpdateMenuItem_HasActivePanel(IMenu menu) { menu.SetDisabled(GetActivePanel() == null); @@ -7852,6 +7865,7 @@ namespace IDE if (!sourceViewPanel.[Friend]mWantsFullRefresh) sourceViewPanel.UpdateQueuedEmitShowData(); + sourceViewPanel.EditWidget?.Content.RemoveSecondaryTextCursors(); return sourceViewPanel; } } @@ -7863,6 +7877,7 @@ namespace IDE svTabButton.mIsTemp = true; sourceViewPanel.ShowHotFileIdx(showHotIdx); sourceViewPanel.ShowFileLocation(refHotIdx, Math.Max(0, line), Math.Max(0, column), hilitePosition); + sourceViewPanel.EditWidget?.Content.RemoveSecondaryTextCursors(); return sourceViewPanel; } @@ -8758,38 +8773,45 @@ namespace IDE return; var ewc = sourceViewPanel.mEditWidget.mEditWidgetContent; - if (!ewc.HasSelection()) - return; - - /*ewc.mSelection.Value.GetAsForwardSelect(var startPos, var endPos); - for (int i = startPos; i < endPos; i++) + for (var cursor in ewc.mTextCursors) { - var c = ref ewc.mData.mText[i].mChar; + ewc.SetTextCursor(cursor); + if (!ewc.HasSelection()) + continue; + + /*ewc.mSelection.Value.GetAsForwardSelect(var startPos, var endPos); + for (int i = startPos; i < endPos; i++) + { + var c = ref ewc.mData.mText[i].mChar; + if (toUpper) + c = c.ToUpper; + else + c = c.ToLower; + }*/ + + var prevSel = ewc.mSelection.Value; + + var str = scope String(); + ewc.GetSelectionText(str); + + var prevStr = scope String(); + prevStr.Append(str); + if (toUpper) - c = c.ToUpper; + str.ToUpper(); else - c = c.ToLower; - }*/ + str.ToLower(); - var prevSel = ewc.mSelection.Value; + if (str == prevStr) + continue; - var str = scope String(); - ewc.GetSelectionText(str); + ewc.CreateMultiCursorUndoBatch("IDEApp.ChangeCase()"); + ewc.InsertAtCursor(str); - var prevStr = scope String(); - prevStr.Append(str); - - if (toUpper) - str.ToUpper(); - else - str.ToLower(); - - if (str == prevStr) - return; - - ewc.InsertAtCursor(str); - - ewc.mSelection = prevSel; + ewc.mSelection = prevSel; + } + ewc.CloseMultiCursorUndoBatch(); + ewc.SetPrimaryTextCursor(); } public bool IsFilteredOut(String fileName) diff --git a/IDE/src/ui/AutoComplete.bf b/IDE/src/ui/AutoComplete.bf index 8eccab69..42a1bb5a 100644 --- a/IDE/src/ui/AutoComplete.bf +++ b/IDE/src/ui/AutoComplete.bf @@ -995,7 +995,7 @@ namespace IDE.ui List textSections = scope List(selectedEntry.mText.Split('\x01')); - int cursorPos = mAutoComplete.mTargetEditWidget.Content.CursorTextPos; + int cursorPos = mAutoComplete.mTargetEditWidget.Content.mTextCursors.Front.mCursorTextPos; for (int sectionIdx = 0; sectionIdx < mAutoComplete.mInvokeSrcPositions.Count - 1; sectionIdx++) { if (cursorPos > mAutoComplete.mInvokeSrcPositions[sectionIdx]) @@ -1441,7 +1441,7 @@ namespace IDE.ui if ((mInvokeWindow != null) && (!mInvokeWidget.mIsAboveText)) { - int textIdx = mTargetEditWidget.Content.CursorTextPos; + int textIdx = mTargetEditWidget.Content.mTextCursors.Front.mCursorTextPos; int line = 0; int column = 0; if (textIdx >= 0) @@ -1559,7 +1559,11 @@ namespace IDE.ui { if ((mInsertEndIdx != -1) && (mInsertStartIdx != -1)) { - mTargetEditWidget.Content.ExtractString(mInsertStartIdx, Math.Max(mInsertEndIdx - mInsertStartIdx, 0), outFilter); + var length = Math.Abs(mInsertEndIdx - mInsertStartIdx); + if (length == 0) + return; + var start = Math.Min(mInsertStartIdx, mInsertEndIdx); + mTargetEditWidget.Content.ExtractString(start, length, outFilter); } } @@ -1567,13 +1571,33 @@ namespace IDE.ui { //Debug.WriteLine("GetAsyncTextPos start {0} {1}", mInsertStartIdx, mInsertEndIdx); - mInsertEndIdx = (int32)mTargetEditWidget.Content.CursorTextPos; - while ((mInsertStartIdx != -1) && (mInsertStartIdx < mInsertEndIdx)) + mInsertEndIdx = (int32)mTargetEditWidget.Content.mTextCursors.Front.mCursorTextPos; + /*while ((mInsertStartIdx != -1) && (mInsertStartIdx < mInsertEndIdx)) { char8 c = (char8)mTargetEditWidget.Content.mData.mText[mInsertStartIdx].mChar; - if ((c != ' ') && (c != ',')) + Debug.WriteLine("StartIdx: {}, EndIdx: {}, mData.mText[startIdx]: '{}'", mInsertStartIdx, mInsertEndIdx, c); + if ((c != ' ') && (c != ',') && (c != '(')) break; mInsertStartIdx++; + }*/ + mInsertStartIdx = mInsertEndIdx-1; + while ((mInsertStartIdx <= mInsertEndIdx) && (mInsertStartIdx > 0)) + { + var data = mTargetEditWidget.Content.mData.mText[mInsertStartIdx - 1]; + var type = (SourceElementType)data.mDisplayTypeId; + var char = data.mChar; + + // Explicit delimeters + if ((char == '\n') || (char == '}') || (char == ';') || (char == '.')) + break; + + if (char.IsWhiteSpace) + break; + + if ((!char.IsLetterOrDigit) && (char != '_') && (type != .Keyword) && (!char.IsWhiteSpace) && (data.mChar != '@')) + break; + + mInsertStartIdx--; } /*mInsertStartIdx = mInsertEndIdx; @@ -2398,6 +2422,11 @@ namespace IDE.ui } continue; } + else if (entryDisplay.Length == 0) + { + // Skip entry that has no name + continue; + } switch (entryType) { @@ -2545,7 +2574,7 @@ namespace IDE.ui }*/ } - if (changedAfterInfo) + if ((changedAfterInfo) || (mTargetEditWidget.Content.mTextCursors.Count > 1)) { GetAsyncTextPos(); } @@ -2557,7 +2586,7 @@ namespace IDE.ui mTargetEditWidget.Content.GetLineCharAtIdx(mInvokeSrcPositions[0], out invokeLine, out invokeColumn); int insertLine = 0; int insertColumn = 0; - mTargetEditWidget.Content.GetLineCharAtIdx(mTargetEditWidget.Content.CursorTextPos, out insertLine, out insertColumn); + mTargetEditWidget.Content.GetLineCharAtIdx(mTargetEditWidget.Content.mTextCursors.Front.mCursorTextPos, out insertLine, out insertColumn); if ((insertLine != invokeLine) && ((insertLine - invokeLine) * gApp.mCodeFont.GetHeight() < GS!(40))) mInvokeWidget.mIsAboveText = true; @@ -2565,7 +2594,7 @@ namespace IDE.ui mInfoFilter = new String(); GetFilter(mInfoFilter); - UpdateData(selectString, changedAfterInfo); + UpdateData(selectString, changedAfterInfo); mPopulating = false; //Debug.WriteLine("SetInfo {0} {1}", mInsertStartIdx, mInsertEndIdx); @@ -2716,7 +2745,7 @@ namespace IDE.ui var targetSourceEditWidgetContent = mTargetEditWidget.Content as SourceEditWidgetContent; var sourceEditWidgetContent = targetSourceEditWidgetContent; - var prevCursorPosition = sourceEditWidgetContent.CursorTextPos; + var prevCursorPosition = sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos; var prevScrollPos = mTargetEditWidget.mVertPos.mDest; UndoBatchStart undoBatchStart = null; @@ -2797,7 +2826,7 @@ namespace IDE.ui return; } - sourceEditWidgetContent.CursorTextPos = fixitIdx; + sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos = fixitIdx; if (focusChange) sourceEditWidgetContent.EnsureCursorVisible(true, true); @@ -2814,7 +2843,7 @@ namespace IDE.ui else InsertImplText(sourceEditWidgetContent, fixitInsert); - fixitIdx = (.)sourceEditWidgetContent.CursorTextPos; + fixitIdx = (.)sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos; insertCount++; } } @@ -2822,9 +2851,9 @@ namespace IDE.ui if (!focusChange) { mTargetEditWidget.VertScrollTo(prevScrollPos, true); - sourceEditWidgetContent.CursorTextPos = prevCursorPosition; + sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos = prevCursorPosition; int addedSize = sourceEditWidgetContent.mData.mTextLength - prevTextLength; - sourceEditWidgetContent.[Friend]AdjustCursorsAfterExternalEdit(fixitIdx, addedSize); + sourceEditWidgetContent.[Friend]AdjustCursorsAfterExternalEdit(fixitIdx, addedSize, 0); } if (historyEntry != null) @@ -2887,9 +2916,9 @@ namespace IDE.ui } if (!sourceEditWidgetContent.IsLineWhiteSpace(sourceEditWidgetContent.CursorLineAndColumn.mLine)) { - int prevPos = sourceEditWidgetContent.CursorTextPos; + int prevPos = sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos; sourceEditWidgetContent.InsertAtCursor("\n"); - sourceEditWidgetContent.CursorTextPos = prevPos; + sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos = (int32)prevPos; } sourceEditWidgetContent.CursorToLineEnd(); } @@ -2906,7 +2935,7 @@ namespace IDE.ui } else if (c == '\b') // Close block { - int cursorPos = sourceEditWidgetContent.CursorTextPos; + int cursorPos = sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos; while (cursorPos < sourceEditWidgetContent.mData.mTextLength) { char8 checkC = sourceEditWidgetContent.mData.mText[cursorPos].mChar; @@ -2914,7 +2943,7 @@ namespace IDE.ui if (checkC == '}') break; } - sourceEditWidgetContent.CursorTextPos = cursorPos; + sourceEditWidgetContent.mTextCursors.Front.mCursorTextPos = (int32)cursorPos; } else { @@ -2946,70 +2975,100 @@ namespace IDE.ui } } - public void InsertSelection(char32 keyChar, String insertType = null, String insertStr = null) - { - //Debug.WriteLine("InsertSelection"); + public void InsertSelection(char32 keyChar, String insertType = null, String insertStr = null) + { + var sewc = mTargetEditWidget.Content as SourceEditWidgetContent; + Debug.Assert(sewc.IsPrimaryTextCursor()); + var isExplicitInsert = (keyChar == '\0') || (keyChar == '\t') || (keyChar == '\n') || (keyChar == '\r'); - EditSelection editSelection = EditSelection(); - editSelection.mStartPos = mInsertStartIdx; - if (mInsertEndIdx != -1) - editSelection.mEndPos = mInsertEndIdx; - else - editSelection.mEndPos = mInsertStartIdx; - - var entry = mAutoCompleteListWidget.mEntryList[mAutoCompleteListWidget.mSelectIdx]; - if (keyChar == '!') - { - if (!entry.mEntryDisplay.EndsWith("!")) - { - // Try to find one that DOES end with a '!' - for (var checkEntry in mAutoCompleteListWidget.mEntryList) - { - if (checkEntry.mEntryDisplay.EndsWith("!")) - entry = checkEntry; - } - } - } + AutoCompleteListWidget.EntryWidget GetEntry() + { + var entry = mAutoCompleteListWidget.mEntryList[mAutoCompleteListWidget.mSelectIdx]; + if ((keyChar == '!') && (!entry.mEntryDisplay.EndsWith("!"))) + { + // Try to find one that DOES end with a '!' + for (var checkEntry in mAutoCompleteListWidget.mEntryList) + { + if (checkEntry.mEntryDisplay.EndsWith("!")) + return checkEntry; + } + } + return entry; + } + + EditSelection CalculateSelection(String implText) + { + var endPos = (int32)sewc.CursorTextPos; + var startPos = endPos; + var wentOverWhitespace = false; + while ((startPos <= endPos) && (startPos > 0)) + { + var data = sewc.mData.mText[startPos - 1]; + var type = (SourceElementType)data.mDisplayTypeId; + + // Explicit delimeters + if ((data.mChar == '\n') || (data.mChar == '}') || (data.mChar == ';') || (data.mChar == '.')) + break; + + var isWhiteSpace = data.mChar.IsWhiteSpace; + var isLetterOrDigit = data.mChar.IsLetterOrDigit; + + // When it's not a override method for example + // we break right after we find a non-letter-or-digit character + // So we would only select last word + if ((implText == null) && (!isLetterOrDigit) && (data.mChar != '_') && (data.mChar != '@')) + break; + + // This is for cases, when we are searching for + if ((!isLetterOrDigit) && (type != .Keyword) && (!isWhiteSpace) && (data.mChar != '_')) + break; + + wentOverWhitespace = isWhiteSpace; + startPos--; + } + + if (wentOverWhitespace) + startPos++; + + return EditSelection(startPos, endPos); + } + + var entry = GetEntry(); if (insertStr != null) insertStr.Append(entry.mEntryInsert ?? entry.mEntryDisplay); if (entry.mEntryType == "fixit") { if (insertType != null) - insertType.Append(entry.mEntryType); + insertType.Append(entry.mEntryType); + sewc.RemoveSecondaryTextCursors(); ApplyFixit(entry.mEntryInsert); return; } - bool isExplicitInsert = (keyChar == '\0') || (keyChar == '\t') || (keyChar == '\n') || (keyChar == '\r'); - - String insertText = entry.mEntryInsert ?? entry.mEntryDisplay; + var insertText = scope String(entry.mEntryInsert ?? entry.mEntryDisplay); if ((!isExplicitInsert) && (insertText.Contains('\t'))) { // Don't insert multi-line blocks unless we have an explicit insert request (click, tab, or enter) return; } - if ((keyChar == '=') && (insertText.EndsWith("="))) + if ((keyChar == '=') && (insertText.EndsWith("="))) insertText.RemoveToEnd(insertText.Length - 1); - //insertText = insertText.Substring(0, insertText.Length - 1); - String implText = null; - int tabIdx = insertText.IndexOf('\t'); - int splitIdx = tabIdx; - int crIdx = insertText.IndexOf('\r'); - if ((crIdx != -1) && (tabIdx != -1) && (crIdx < tabIdx)) - splitIdx = crIdx; - if (splitIdx != -1) - { - implText = scope:: String(); - implText.Append(insertText, splitIdx); - insertText.RemoveToEnd(splitIdx); - } - String prevText = scope String(); - mTargetEditWidget.Content.ExtractString(editSelection.mStartPos, editSelection.mEndPos - editSelection.mStartPos, prevText); - //sAutoCompleteMRU[insertText] = sAutoCompleteIdx++; + // Save persistent text positions + PersistentTextPosition[] persistentInvokeSrcPositons = null; + if (mInvokeSrcPositions != null) + { + persistentInvokeSrcPositons = scope:: PersistentTextPosition[mInvokeSrcPositions.Count]; + for (int32 i = 0; i < mInvokeSrcPositions.Count; i++) + { + persistentInvokeSrcPositons[i] = new PersistentTextPosition(mInvokeSrcPositions[i]); + sewc.PersistentTextPositions.Add(persistentInvokeSrcPositons[i]); + } + } + String* keyPtr; int32* valuePtr; if (sAutoCompleteMRU.TryAdd(entry.mEntryDisplay, out keyPtr, out valuePtr)) @@ -3019,64 +3078,68 @@ namespace IDE.ui } *valuePtr = sAutoCompleteIdx++; - if (insertText == prevText) - return; + String implText = null; + int tabIdx = insertText.IndexOf('\t'); + int splitIdx = tabIdx; + int crIdx = insertText.IndexOf('\r'); + if ((crIdx != -1) && (tabIdx != -1) && (crIdx < tabIdx)) + splitIdx = crIdx; + if (splitIdx != -1) + { + implText = scope:: String(); + implText.Append(insertText, splitIdx); + insertText.RemoveToEnd(splitIdx); + } - var sourceEditWidgetContent = mTargetEditWidget.Content as SourceEditWidgetContent; + for (var cursor in sewc.mTextCursors) + { + sewc.SetTextCursor(cursor); + var editSelection = CalculateSelection(implText); - PersistentTextPosition[] persistentInvokeSrcPositons = null; - if (mInvokeSrcPositions != null) - { - persistentInvokeSrcPositons = scope:: PersistentTextPosition[mInvokeSrcPositions.Count]; - for (int32 i = 0; i < mInvokeSrcPositions.Count; i++) - { - persistentInvokeSrcPositons[i] = new PersistentTextPosition(mInvokeSrcPositions[i]); - sourceEditWidgetContent.PersistentTextPositions.Add(persistentInvokeSrcPositons[i]); - } - } + var prevText = scope String(); + sewc.ExtractString(editSelection.MinPos, editSelection.Length, prevText); + if ((prevText.Length > 0) && (insertText == prevText)) + continue; - mTargetEditWidget.Content.mSelection = editSelection; - //bool isMethod = (entry.mEntryType == "method"); - if (insertText.EndsWith("<>")) - { - if (keyChar == '\t') - mTargetEditWidget.Content.InsertCharPair(insertText); - else if (keyChar == '<') + sewc.mSelection = editSelection; + sewc.mCursorTextPos = (int32)editSelection.MaxPos; + + if (insertText.EndsWith("<>")) { - String str = scope String(); - str.Append(insertText, 0, insertText.Length - 2); - mTargetEditWidget.Content.InsertAtCursor(str, .NoRestoreSelectionOnUndo); + if (keyChar == '\t') + sewc.InsertCharPair(insertText); + else if (keyChar == '<') + { + var str = scope String(); + str.Append(insertText, 0, insertText.Length - 2); + sewc.InsertAtCursor(str, .NoRestoreSelectionOnUndo); + } + else + sewc.InsertAtCursor(insertText, .NoRestoreSelectionOnUndo); } - else - mTargetEditWidget.Content.InsertAtCursor(insertText, .NoRestoreSelectionOnUndo); - } - else - mTargetEditWidget.Content.InsertAtCursor(insertText, .NoRestoreSelectionOnUndo); + else + sewc.InsertAtCursor(insertText, .NoRestoreSelectionOnUndo); - /*if (mIsAsync) - UpdateAsyncInfo();*/ + if (implText != null) + InsertImplText(sewc, implText); + } - if (implText != null) - InsertImplText(sourceEditWidgetContent, implText); - - if (persistentInvokeSrcPositons != null) - { - for (int32 i = 0; i < mInvokeSrcPositions.Count; i++) - { - //TEST - //var persistentTextPositon = persistentInvokeSrcPositons[i + 100]; + // Load persistent text positions back + if (persistentInvokeSrcPositons != null) + { + for (int32 i = 0; i < mInvokeSrcPositions.Count; i++) + { var persistentTextPositon = persistentInvokeSrcPositons[i]; - mInvokeSrcPositions[i] = persistentTextPositon.mIndex; - sourceEditWidgetContent.PersistentTextPositions.Remove(persistentTextPositon); + mInvokeSrcPositions[i] = persistentTextPositon.mIndex; + sewc.PersistentTextPositions.Remove(persistentTextPositon); delete persistentTextPositon; - } - } + } + } + + sewc.SetPrimaryTextCursor(); + sewc.EnsureCursorVisible(); + } - mTargetEditWidget.Content.EnsureCursorVisible(); - if ((insertType != null) && (insertText.Length > 0)) - insertType.Append(entry.mEntryType); - } - public void MarkDirty() { if (mInvokeWidget != null) diff --git a/IDE/src/ui/QuickFind.bf b/IDE/src/ui/QuickFind.bf index 40046394..7a33102f 100644 --- a/IDE/src/ui/QuickFind.bf +++ b/IDE/src/ui/QuickFind.bf @@ -225,6 +225,13 @@ namespace IDE.ui if (evt.mKeyCode == KeyCode.Return) { + var keyFlags = mWidgetWindow.GetKeyFlags(true); + if (keyFlags.HasFlag(.Alt)) + { + SelectAllMatches(keyFlags.HasFlag(.Shift)); + Close(); + return; + } if (evt.mSender == mFindEditWidget) { FindNext(1, true); @@ -249,6 +256,108 @@ namespace IDE.ui DoReplace(true); } + bool SelectAllMatches(bool caseSensitive) + { + var ewc = mEditWidget.Content; + var sewc = ewc as SourceEditWidgetContent; + ewc.SetPrimaryTextCursor(); + + String findText = scope String(); + mFindEditWidget.GetText(findText); + sLastSearchString.Set(findText); + if (findText.Length == 0) + return false; + + String findTextLower = scope String(findText); + findTextLower.ToLower(); + String findTextUpper = scope String(findText); + findTextUpper.ToUpper(); + + int32 selStart = (mSelectionStart != null) ? mSelectionStart.mIndex : 0; + int32 selEnd = (mSelectionEnd != null) ? mSelectionEnd.mIndex : ewc.mData.mTextLength; + + + bool Matches(int32 idx) + { + if (idx + findText.Length >= ewc.mData.mTextLength) + return false; + + for (var i = 0; i < findText.Length; i++) + { + var char = ewc.mData.mText[idx+i].mChar; + + if (caseSensitive) + { + if (findText[i] != char) + return false; + } + else if ((findTextLower[i] != char) && (findTextUpper[i] != char)) + { + return false; + } + } + + return true; + } + + var primaryCursor = ewc.mTextCursors.Front; + var initialCursorPos = primaryCursor.mCursorTextPos; + EditWidgetContent.TextCursor swapCursor = null; + + ewc.RemoveSecondaryTextCursors(); + + primaryCursor.mJustInsertedCharPair = false; + primaryCursor.mCursorImplicitlyMoved = false; + primaryCursor.mVirtualCursorPos = null; + + var isFirstMatch = true; + + for (var idx = selStart; idx < selEnd; idx++) + { + if (!Matches(idx)) + continue; + + if (!isFirstMatch) + { + var cursor = ewc.mTextCursors.Add(.. new EditWidgetContent.TextCursor(-1, primaryCursor)); + if (cursor.mCursorTextPos == initialCursorPos) + swapCursor = cursor; + } + + isFirstMatch = false; + + primaryCursor.mCursorTextPos = (int32)(idx + findText.Length); + primaryCursor.mSelection = EditSelection(idx, primaryCursor.mCursorTextPos); + + idx += (int32)findText.Length; + } + + // Remove selection when at least one match has been found + if ((sewc != null) && (!isFirstMatch)) + { + if (mSelectionStart != null) + { + sewc.PersistentTextPositions.Remove(mSelectionStart); + DeleteAndNullify!(mSelectionStart); + } + + if (mSelectionEnd != null) + { + sewc.PersistentTextPositions.Remove(mSelectionEnd); + DeleteAndNullify!(mSelectionEnd); + } + } + + // Making sure that primary cursor is at the position where QuickFind found first match. + if (swapCursor != null) + { + Swap!(primaryCursor.mCursorTextPos, swapCursor.mCursorTextPos); + Swap!(primaryCursor.mSelection.Value, swapCursor.mSelection.Value); + } + + return (!isFirstMatch); + } + void EditWidgetSubmit(EditEvent editEvent) { //FindNext(true); diff --git a/IDE/src/ui/SourceEditWidgetContent.bf b/IDE/src/ui/SourceEditWidgetContent.bf index 8a145c72..9ffdc283 100644 --- a/IDE/src/ui/SourceEditWidgetContent.bf +++ b/IDE/src/ui/SourceEditWidgetContent.bf @@ -776,6 +776,7 @@ namespace IDE.ui public SourceViewPanel mSourceViewPanel; //public bool mAsyncAutocomplete; public bool mIsInKeyChar; + public bool mDidAutoComplete; public bool mDbgDoTest; public int32 mCursorStillTicks; public static bool sReadOnlyErrorShown; @@ -1893,6 +1894,7 @@ namespace IDE.ui bool startsWithNewline = (forceMatchIndent) && (str.StartsWith("\n")); bool isMultiline = str.Contains("\n"); + CreateMultiCursorUndoBatch("SEWC.PasteText(str, forceMatchIndent)"); if (startsWithNewline || isMultiline) { var undoBatchStart = new UndoBatchStart("pasteText"); @@ -2222,6 +2224,7 @@ namespace IDE.ui public void ScopePrev() { + RemoveSecondaryTextCursors(); int pos = CursorTextPos - 1; int openCount = 0; @@ -2254,6 +2257,7 @@ namespace IDE.ui public void ScopeNext() { + RemoveSecondaryTextCursors(); int pos = CursorTextPos; int openCount = 0; @@ -2297,6 +2301,7 @@ namespace IDE.ui public bool OpenCodeBlock() { + CreateMultiCursorUndoBatch("SEWC.OpenCodeBlock()"); int lineIdx; if (HasSelection()) @@ -2843,78 +2848,73 @@ namespace IDE.ui public bool CommentBlock() { - bool? doComment = true; - if (CheckReadOnly()) return false; - var startLineAndCol = CursorLineAndColumn; - int startTextPos = CursorTextPos; - var prevSelection = mSelection; - bool hadSelection = HasSelection(); - - if ((!HasSelection()) && (doComment != null)) + for (var cursor in mTextCursors) { - CursorToLineEnd(); - int cursorEndPos = CursorTextPos; - CursorToLineStart(false); - mSelection = .(CursorTextPos, cursorEndPos); - } + SetTextCursor(cursor); - if ((HasSelection()) && (mSelection.Value.Length > 1)) - { - UndoBatchStart undoBatchStart = new UndoBatchStart("embeddedCommentBlock"); - mData.mUndoManager.Add(undoBatchStart); + var startLineAndCol = CursorLineAndColumn; + int startTextPos = CursorTextPos; + var prevSelection = mSelection; + bool hadSelection = HasSelection(); - var setCursorAction = new SetCursorAction(this); - setCursorAction.mSelection = prevSelection; - setCursorAction.mCursorTextPos = (.)startTextPos; - mData.mUndoManager.Add(setCursorAction); - - int minPos = mSelection.GetValueOrDefault().MinPos; - int maxPos = mSelection.GetValueOrDefault().MaxPos; - mSelection = null; - - var str = scope String(); - ExtractString(minPos, maxPos - minPos, str); - - var trimmedStr = scope String(); - trimmedStr.Append(str); - int32 startLen = (int32)trimmedStr.Length; - trimmedStr.TrimStart(); - int32 afterTrimStart = (int32)trimmedStr.Length; - trimmedStr.TrimEnd(); - int32 afterTrimEnd = (int32)trimmedStr.Length; - - int firstCharPos = minPos + (startLen - afterTrimStart); - int lastCharPos = maxPos - (afterTrimStart - afterTrimEnd); - - if (doComment != false) + if (!HasSelection()) { + CursorToLineEnd(); + int cursorEndPos = CursorTextPos; + CursorToLineStart(false); + mSelection = EditSelection(CursorTextPos, cursorEndPos); + } + + if (HasSelection()) + { + CreateMultiCursorUndoBatch("SEWC.CommentBlock()"); + var setCursorAction = new SetCursorAction(this); + setCursorAction.mSelection = prevSelection; + setCursorAction.mCursorTextPos = (int32)startTextPos; + mData.mUndoManager.Add(setCursorAction); + + int minPos = mSelection.GetValueOrDefault().MinPos; + int maxPos = mSelection.GetValueOrDefault().MaxPos; + mSelection = null; + + var str = scope String(); + ExtractString(minPos, maxPos - minPos, str); + + var trimmedStr = scope String(); + trimmedStr.Append(str); + int32 startLen = (int32)trimmedStr.Length; + trimmedStr.TrimStart(); + int32 afterTrimStart = (int32)trimmedStr.Length; + trimmedStr.TrimEnd(); + int32 afterTrimEnd = (int32)trimmedStr.Length; + + int firstCharPos = minPos + (startLen - afterTrimStart); + int lastCharPos = maxPos - (afterTrimStart - afterTrimEnd); + CursorTextPos = firstCharPos; InsertAtCursor("/*"); CursorTextPos = lastCharPos + 2; InsertAtCursor("*/"); - if (doComment != null) - mSelection = EditSelection(firstCharPos, lastCharPos + 4); + mSelection = EditSelection(firstCharPos, lastCharPos + 4); + + if (startTextPos <= minPos) + CursorLineAndColumn = startLineAndCol; + else if (startTextPos < maxPos) + CursorTextPos = startTextPos + 2; + + if (!hadSelection) + mSelection = null; } - - if (undoBatchStart != null) - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); - - if (startTextPos <= minPos) - CursorLineAndColumn = startLineAndCol; - else if (startTextPos < maxPos) - CursorTextPos = startTextPos + 2; - - if ((doComment == null) || (!hadSelection)) - mSelection = null; - - return true; } - return false; + CloseMultiCursorUndoBatch(); + SetPrimaryTextCursor(); + + return true; } public bool CommentLines() @@ -2922,157 +2922,167 @@ namespace IDE.ui if (CheckReadOnly()) return false; - int startTextPos = CursorTextPos; - var prevSelection = mSelection; - bool hadSelection = HasSelection(); - var startLineAndCol = CursorLineAndColumn; - if (!HasSelection()) + var sortedCursors = mTextCursors; + if (mTextCursors.Count > 1) + sortedCursors = GetSortedCursors(.. scope :: List()); + + // Forcing creation of undo batch, because even single cursor has + // multiple undo-actions. + CreateMultiCursorUndoBatch("SEWC.CommentLines()", force: true); + + for (var cursor in sortedCursors) { - CursorToLineEnd(); - int cursorEndPos = CursorTextPos; - CursorToLineStart(false); - mSelection = .(CursorTextPos, cursorEndPos); - } + SetTextCursor(cursor); - UndoBatchStart undoBatchStart = new UndoBatchStart("embeddedCommentLines"); - mData.mUndoManager.Add(undoBatchStart); - - var setCursorAction = new SetCursorAction(this); - setCursorAction.mSelection = prevSelection; - setCursorAction.mCursorTextPos = (.)startTextPos; - mData.mUndoManager.Add(setCursorAction); - - int minPos = mSelection.GetValueOrDefault().MinPos; - int maxPos = mSelection.GetValueOrDefault().MaxPos; - mSelection = null; - - while (minPos > 0) - { - var c = mData.mText[minPos - 1].mChar; - if (c == '\n') - break; - minPos--; - } - - bool hadMaxChar = false; - int checkMaxPos = maxPos; - while (checkMaxPos > 0) - { - var c = mData.mText[checkMaxPos - 1].mChar; - if (c == '\n') - break; - if ((c != '\t') && (c != ' ')) + var startTextPos = CursorTextPos; + var prevSelection = mSelection; + var hadSelection = HasSelection(); + var startLineAndCol = CursorLineAndColumn; + if (!HasSelection()) { - hadMaxChar = true; - break; + CursorToLineEnd(); + int cursorEndPos = CursorTextPos; + CursorToLineStart(false); + mSelection = .(CursorTextPos, cursorEndPos); } - checkMaxPos--; - } - if (!hadMaxChar) - { - checkMaxPos = maxPos; - while (checkMaxPos < mData.mTextLength) + var setCursorAction = new SetCursorAction(this); + setCursorAction.mSelection = prevSelection; + setCursorAction.mCursorTextPos = (int32)startTextPos; + mData.mUndoManager.Add(setCursorAction); + + int minPos = mSelection.Value.MinPos; + int maxPos = mSelection.Value.MaxPos; + mSelection = null; + + while (minPos > 0) { - var c = mData.mText[checkMaxPos].mChar; + var c = mData.mText[minPos - 1].mChar; + if (c == '\n') + break; + minPos--; + } + + bool hadMaxChar = false; + int checkMaxPos = maxPos; + while (checkMaxPos > 0) + { + var c = mData.mText[checkMaxPos - 1].mChar; if (c == '\n') break; if ((c != '\t') && (c != ' ')) { - maxPos = checkMaxPos + 1; + hadMaxChar = true; break; } - checkMaxPos++; + checkMaxPos--; } - } - - int wantLineCol = -1; - int lineStartCol = 0; - bool didLineComment = false; - for (int i = minPos; i < maxPos; i++) - { - var c = mData.mText[i].mChar; - if (didLineComment) + if (!hadMaxChar) { - if (c == '\n') + checkMaxPos = maxPos; + while (checkMaxPos < mData.mTextLength) { - didLineComment = false; - lineStartCol = 0; + var c = mData.mText[checkMaxPos].mChar; + if (c == '\n') + break; + if ((c != '\t') && (c != ' ')) + { + maxPos = checkMaxPos + 1; + break; + } + checkMaxPos++; } - continue; } - if (c == '\t') - lineStartCol += gApp.mSettings.mEditorSettings.mTabSize; - else if (c == ' ') - lineStartCol++; - else - { - if (wantLineCol == -1) - wantLineCol = lineStartCol; - else - wantLineCol = Math.Min(wantLineCol, lineStartCol); - didLineComment = true; - } - } - wantLineCol = Math.Max(0, wantLineCol); - didLineComment = false; - lineStartCol = 0; - int appendedCount = 0; - for (int i = minPos; i < maxPos; i++) - { - var c = mData.mText[i].mChar; - if (didLineComment) + int wantLineCol = -1; + int lineStartCol = 0; + bool didLineComment = false; + + for (int i = minPos; i < maxPos; i++) { - if (c == '\n') + var c = mData.mText[i].mChar; + if (didLineComment) { - didLineComment = false; - lineStartCol = 0; + if (c == '\n') + { + didLineComment = false; + lineStartCol = 0; + } + continue; } - continue; - } - - bool commentNow = false; - if ((wantLineCol != -1) && (lineStartCol >= wantLineCol)) - commentNow = true; - - if (c == '\t') - lineStartCol += gApp.mSettings.mEditorSettings.mTabSize; - else if (c == ' ') - lineStartCol++; - else - commentNow = true; - - if (commentNow) - { - CursorTextPos = i; - String str = scope .(); - while (lineStartCol + gApp.mSettings.mEditorSettings.mTabSize <= wantLineCol) - { + if (c == '\t') lineStartCol += gApp.mSettings.mEditorSettings.mTabSize; - str.Append("\t"); + else if (c == ' ') + lineStartCol++; + else + { + if (wantLineCol == -1) + wantLineCol = lineStartCol; + else + wantLineCol = Math.Min(wantLineCol, lineStartCol); + didLineComment = true; } - str.Append("//"); - InsertAtCursor(str); - didLineComment = true; - maxPos += str.Length; - if (i <= startTextPos + appendedCount) - appendedCount += str.Length; } + wantLineCol = Math.Max(0, wantLineCol); + + didLineComment = false; + lineStartCol = 0; + int appendedCount = 0; + for (int i = minPos; i < maxPos; i++) + { + var c = mData.mText[i].mChar; + if (didLineComment) + { + if (c == '\n') + { + didLineComment = false; + lineStartCol = 0; + } + continue; + } + + bool commentNow = false; + if ((wantLineCol != -1) && (lineStartCol >= wantLineCol)) + commentNow = true; + + if (c == '\t') + lineStartCol += gApp.mSettings.mEditorSettings.mTabSize; + else if (c == ' ') + lineStartCol++; + else + commentNow = true; + + if (commentNow) + { + CursorTextPos = i; + String str = scope .(); + while (lineStartCol + gApp.mSettings.mEditorSettings.mTabSize <= wantLineCol) + { + lineStartCol += gApp.mSettings.mEditorSettings.mTabSize; + str.Append("\t"); + } + str.Append("//"); + InsertAtCursor(str); + didLineComment = true; + maxPos += str.Length; + if (i <= startTextPos + appendedCount) + appendedCount += str.Length; + } + } + mSelection = EditSelection(minPos, maxPos); + + if (appendedCount > 0) + CursorTextPos = startTextPos + appendedCount; + else + CursorLineAndColumn = startLineAndCol; + + if (!hadSelection) + mSelection = null; } - mSelection = EditSelection(minPos, maxPos); - if (undoBatchStart != null) - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); - - if (appendedCount > 0) - CursorTextPos = startTextPos + appendedCount; - else - CursorLineAndColumn = startLineAndCol; - - if (!hadSelection) - mSelection = null; + CloseMultiCursorUndoBatch(); + SetPrimaryTextCursor(); return true; } @@ -3095,144 +3105,168 @@ namespace IDE.ui if (CheckReadOnly()) return false; - int startTextPos = CursorTextPos; - bool doLineComment = false; - var prevSelection = mSelection; + var sortedCursors = mTextCursors; + if (mTextCursors.Count > 1) + sortedCursors = GetSortedCursors(.. scope :: List()); - LineAndColumn? startLineAndCol = CursorLineAndColumn; - if (!HasSelection()) + var didComment = false; + for (var cursor in sortedCursors) { - CursorToLineEnd(); - int cursorEndPos = CursorTextPos; - CursorToLineStart(false); - mSelection = .(CursorTextPos, cursorEndPos); - doLineComment = true; - } + SetTextCursor(cursor); - if ((HasSelection()) && (mSelection.Value.Length > 0)) - { - UndoBatchStart undoBatchStart = new UndoBatchStart("embeddedToggleComment"); - mData.mUndoManager.Add(undoBatchStart); + int startTextPos = CursorTextPos; + bool doLineComment = false; + var prevSelection = mSelection; + var cursorAtEndPos = true; - var setCursorAction = new SetCursorAction(this); - setCursorAction.mSelection = prevSelection; - setCursorAction.mCursorTextPos = (.)startTextPos; - mData.mUndoManager.Add(setCursorAction); - - int minPos = mSelection.GetValueOrDefault().MinPos; - int maxPos = mSelection.GetValueOrDefault().MaxPos; - mSelection = null; - - var str = scope String(); - ExtractString(minPos, maxPos - minPos, str); - var trimmedStr = scope String(); - trimmedStr.Append(str); - int32 startLen = (int32)trimmedStr.Length; - trimmedStr.TrimStart(); - int32 afterTrimStart = (int32)trimmedStr.Length; - trimmedStr.TrimEnd(); - int32 afterTrimEnd = (int32)trimmedStr.Length; - trimmedStr.Append('\n'); - - int firstCharPos = minPos + (startLen - afterTrimStart); - int lastCharPos = maxPos - (afterTrimStart - afterTrimEnd); - - if (afterTrimEnd == 0) + LineAndColumn? startLineAndCol = CursorLineAndColumn; + if (!HasSelection()) { - if (undoBatchStart != null) - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); - - CursorLineAndColumn = startLineAndCol.Value; - - if (doComment == null) - mSelection = null; - - return false; // not sure if this should be false in blank/only whitespace selection case - } - else if ((doComment != true) && (trimmedStr.StartsWith("//"))) - { - for (int i = firstCharPos; i <= lastCharPos; i++) - { - if (((minPos == 0) && (i == 0)) || - ((minPos >= 0) && (SafeGetChar(i - 1) == '\n') || (SafeGetChar(i - 1) == '\t') || (SafeGetChar(i - 1) == ' '))) - { - if (SafeGetChar(i - 0) == '/' && SafeGetChar(i + 1) == '/') - { - mSelection = EditSelection(i - 0, i + 2); - DeleteSelection(); - lastCharPos -= 2; - while (i < maxPos && SafeGetChar(i) != '\n') - { - i++; - } - } - } - } - - startLineAndCol = null; CursorToLineEnd(); int cursorEndPos = CursorTextPos; - mSelection = .(minPos, cursorEndPos); - } - else if ((doComment != true) && (trimmedStr.StartsWith("/*"))) - { - if (trimmedStr.EndsWith("*/\n")) - { - mSelection = EditSelection(firstCharPos, firstCharPos + 2); - DeleteChar(); - mSelection = EditSelection(lastCharPos - 4, lastCharPos - 2); - DeleteChar(); - - if (prevSelection != null) - mSelection = EditSelection(firstCharPos, lastCharPos - 4); - } - } - else if (doComment != false) - { - //if selection is from beginning of the line then we want to use // comment, that's why the check for line count and ' ' and tab - if (doLineComment) - { - CursorTextPos = minPos; - InsertAtCursor("//"); //goes here if no selection - } - else - { - CursorTextPos = firstCharPos; - InsertAtCursor("/*"); - CursorTextPos = lastCharPos + 2; - InsertAtCursor("*/"); - } - - mSelection = EditSelection(firstCharPos, lastCharPos + 4); - if (startTextPos <= minPos) - CursorLineAndColumn = startLineAndCol.Value; - else - CursorTextPos = startTextPos + 2; - startLineAndCol = null; + CursorToLineStart(false); + mSelection = EditSelection(CursorTextPos, cursorEndPos); + doLineComment = true; } else { - mSelection = prevSelection; + cursorAtEndPos = (mSelection.Value.mStartPos == mCursorTextPos); } - if (undoBatchStart != null) - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); + if (HasSelection()) + { + CreateMultiCursorUndoBatch("SEWC.ToggleComment()", force: true); - if (startLineAndCol != null) - CursorLineAndColumn = startLineAndCol.Value; + var setCursorAction = new SetCursorAction(this); + setCursorAction.mSelection = prevSelection; + setCursorAction.mCursorTextPos = (int32)startTextPos; + mData.mUndoManager.Add(setCursorAction); - if (prevSelection == null) + var minPos = mSelection.GetValueOrDefault().MinPos; + var maxPos = mSelection.GetValueOrDefault().MaxPos; mSelection = null; - ClampCursor(); - FixSelection(); + var str = scope String(); + ExtractString(minPos, (maxPos - minPos), str); + var trimmedStr = scope String(); + trimmedStr.Append(str); + int32 startLen = (int32)trimmedStr.Length; + trimmedStr.TrimStart(); + int32 afterTrimStart = (int32)trimmedStr.Length; + trimmedStr.TrimEnd(); + int32 afterTrimEnd = (int32)trimmedStr.Length; + trimmedStr.Append('\n'); + + int firstCharPos = minPos + (startLen - afterTrimStart); + int lastCharPos = maxPos - (afterTrimStart - afterTrimEnd); + + if (afterTrimEnd == 0) + { + CursorLineAndColumn = startLineAndCol.Value; + + if (doComment == null) + mSelection = null; + + //return false; // not sure if this should be false in blank/only whitespace selection case + continue; + } + else if ((doComment != true) && (trimmedStr.StartsWith("//"))) + { + didComment = true; + for (int i = firstCharPos; i <= lastCharPos; i++) + { + if (((minPos == 0) && (i == 0)) || + ((minPos >= 0) && (SafeGetChar(i - 1) == '\n') || (SafeGetChar(i - 1) == '\t') || (SafeGetChar(i - 1) == ' '))) + { + if (SafeGetChar(i - 0) == '/' && SafeGetChar(i + 1) == '/') + { + mSelection = EditSelection(i - 0, i + 2); + DeleteSelection(); + lastCharPos -= 2; + while (i < maxPos && SafeGetChar(i) != '\n') + { + i++; + } + } + } + } + + startLineAndCol = null; + CursorToLineEnd(); + int cursorEndPos = CursorTextPos; + mSelection = .(minPos, cursorEndPos); + } + else if ((doComment != true) && trimmedStr.StartsWith("/*")) + { + didComment = true; + if (trimmedStr.EndsWith("*/\n")) + { + mSelection = EditSelection(firstCharPos, firstCharPos + 2); + DeleteChar(); + mSelection = EditSelection(lastCharPos - 4, lastCharPos - 2); + DeleteChar(); + + if (prevSelection != null) + mSelection = EditSelection(firstCharPos, lastCharPos - 4); + } + } + else if (doComment != false) + { + didComment = true; + //if selection is from beginning of the line then we want to use // comment, that's why the check for line count and ' ' and tab + if (doLineComment) + { + CursorTextPos = minPos; + InsertAtCursor("//"); //goes here if no selection + } + else + { + CursorTextPos = firstCharPos; + InsertAtCursor("/*"); + CursorTextPos = lastCharPos + 2; + InsertAtCursor("*/"); + } + + mSelection = EditSelection(firstCharPos, lastCharPos + 4); + if (startTextPos <= minPos) + CursorLineAndColumn = startLineAndCol.Value; + else + CursorTextPos = startTextPos + 2; + startLineAndCol = null; + } + else + { + mSelection = prevSelection; + } + + if (startLineAndCol != null) + CursorLineAndColumn = startLineAndCol.Value; + + if (prevSelection == null) + mSelection = null; + + ClampCursor(); + FixSelection(); + + if (mSelection.HasValue) + { + // Placing cursor where it was before, meaning + // at the start or at the end of the selection. + mCursorTextPos = (cursorAtEndPos) + ? (int32)mSelection.Value.mStartPos + : (int32)mSelection.Value.mEndPos + ; + } + } - return true; } - return false; + CloseMultiCursorUndoBatch(); + SetPrimaryTextCursor(); + + return (didComment); } - + public void DeleteAllRight() { int startPos; @@ -3273,22 +3307,45 @@ namespace IDE.ui if ((CheckReadOnly()) || (!mAllowVirtualCursor)) return; - UndoBatchStart undoBatchStart = new UndoBatchStart("duplicateLine"); - mData.mUndoManager.Add(undoBatchStart); + var lineText = scope String(); + var sortedCursors = mTextCursors; + if (mTextCursors.Count > 1) + sortedCursors = GetSortedCursors(.. scope :: List()); - mData.mUndoManager.Add(new SetCursorAction(this)); + // Forcing creation of undo batch, because even single cursor has + // multiple undo-actions (SetCursorAction + InsertTextAction). + CreateMultiCursorUndoBatch("SEWC.DuplicateLine()", force: true); - var prevCursorLineAndColumn = CursorLineAndColumn; - int lineNum = CursorLineAndColumn.mLine; - GetLinePosition(lineNum, var lineStart, var lineEnd); - var str = scope String(); - GetLineText(lineNum, str); - mSelection = null; - CursorLineAndColumn = LineAndColumn(lineNum, 0); - PasteText(str, "line"); - CursorLineAndColumn = LineAndColumn(prevCursorLineAndColumn.mLine + 1, prevCursorLineAndColumn.mColumn); + for (var cursor in sortedCursors.Reversed) + { + SetTextCursor(cursor); + mData.mUndoManager.Add(new SetCursorAction(this)); - mData.mUndoManager.Add(undoBatchStart.mBatchEnd); + var line = CursorLineAndColumn.mLine; + var column = CursorLineAndColumn.mColumn; + var prevCursorPos = mCursorTextPos; + + lineText.Clear(); + GetLineText(line, lineText); + + mSelection = null; + CursorLineAndColumn = LineAndColumn(line+1, 0); + + InsertAtCursor("\n"); + CursorLineAndColumn = LineAndColumn(line+1, 0); + + InsertAtCursor(lineText); + CursorLineAndColumn = LineAndColumn(line+1, column); + + // Forcing calculation of CursorTextPos, + // if this cursor had one before. + if (prevCursorPos != -1) + var _ = CursorTextPos; + } + + CloseMultiCursorUndoBatch(); + SetPrimaryTextCursor(); + EnsureCursorVisible(); } enum StatementRangeFlags @@ -3529,6 +3586,7 @@ namespace IDE.ui void MoveSelection(int toLinePos, bool isStatementAware) { + RemoveSecondaryTextCursors(); /*if (GetStatementRange(CursorTextPos, var startIdx, var endIdx)) { mSelection = .(startIdx, endIdx); @@ -3685,6 +3743,7 @@ namespace IDE.ui public void MoveLine(VertDir dir) { + RemoveSecondaryTextCursors(); int lineNum = CursorLineAndColumn.mLine; if ((dir == .Up && lineNum < 1) || (dir == .Down && lineNum >= GetLineCount() - 1)) @@ -3713,6 +3772,7 @@ namespace IDE.ui public void MoveStatement(VertDir dir) { + RemoveSecondaryTextCursors(); int lineNum = CursorLineAndColumn.mLine; int origLineNum = lineNum; GetLinePosition(lineNum, var lineStart, var lineEnd); @@ -3795,6 +3855,7 @@ namespace IDE.ui void InsertCharPair(String charPair) { + CreateMultiCursorUndoBatch("SEWC.InsertCharPair()"); base.InsertCharPair(charPair); mCurParenPairIdSet.Add(mData.mNextCharId - 2); } @@ -3803,12 +3864,18 @@ namespace IDE.ui { scope AutoBeefPerf("SEWC.KeyChar"); + if (IsPrimaryTextCursor()) + mDidAutoComplete = false; + var keyChar; if (mIgnoreKeyChar) { - mIgnoreKeyChar = false; + // Only flip the flag when we are processing last cursor + if (mTextCursors.Back.mId == mCurrentTextCursor.mId) + mIgnoreKeyChar = false; + return; } @@ -3825,6 +3892,10 @@ namespace IDE.ui ((keyChar == '\r') && (autoCompleteOnEnter))) && (!mWidgetWindow.IsKeyDown(.Shift)); + // Skip completion-char for secondary cursors when AutoComplete just happened + if ((isCompletionChar) && (!IsPrimaryTextCursor()) && (mDidAutoComplete)) + return; + if ((gApp.mSymbolReferenceHelper != null) && (gApp.mSymbolReferenceHelper.IsRenaming)) { if ((keyChar == '\r') || (keyChar == '\n')) @@ -3883,7 +3954,7 @@ namespace IDE.ui } bool forceAutoCompleteInsert = false; - if (isEndingChar) + if ((IsPrimaryTextCursor()) && (isEndingChar)) { bool forceAsyncFinish = false; if (mCursorTextPos > 0) @@ -3927,11 +3998,11 @@ namespace IDE.ui } else { - if ((doAutocomplete) && (mOnFinishAsyncAutocomplete != null)) + if ((IsPrimaryTextCursor()) && (doAutocomplete) && (mOnFinishAsyncAutocomplete != null)) mOnFinishAsyncAutocomplete(); } - if ((mAutoComplete != null) && (mAutoComplete.mAutoCompleteListWidget != null)) + if ((IsPrimaryTextCursor()) && (mAutoComplete != null) && (mAutoComplete.mAutoCompleteListWidget != null)) { if ((mAutoComplete.mInsertEndIdx != -1) && (mAutoComplete.mInsertEndIdx != mCursorTextPos) && (keyChar != '\t') && (keyChar != '\r') && (keyChar != '\n')) doAutocomplete = false; @@ -3965,6 +4036,8 @@ namespace IDE.ui if (mOnFinishAsyncAutocomplete != null) mOnFinishAsyncAutocomplete(); + mDidAutoComplete = true; + CreateMultiCursorUndoBatch("SEWC.KeyChar(autocomplete)"); UndoBatchStart undoBatchStart = new UndoBatchStart("autocomplete"); mData.mUndoManager.Add(undoBatchStart); @@ -4033,6 +4106,7 @@ namespace IDE.ui if (((keyChar == '\n') || (keyChar == '\r')) && (!HasSelection()) && (mIsMultiline) && (!CheckReadOnly())) { + CreateMultiCursorUndoBatch("SEWC.KeyChar(\n||\r)"); UndoBatchStart undoBatchStart = new UndoBatchStart("newline"); mData.mUndoManager.Add(undoBatchStart); @@ -4133,7 +4207,7 @@ namespace IDE.ui } } - if ((mAutoComplete != null) && (mAutoComplete.mInvokeWidget != null)) + if ((IsPrimaryTextCursor()) && (mAutoComplete != null) && (mAutoComplete.mInvokeWidget != null)) { // Update the position of the invoke widget if (IsCursorVisible(false)) @@ -4149,11 +4223,13 @@ namespace IDE.ui } else { - if (mAutoComplete != null) + if (IsPrimaryTextCursor() && mAutoComplete != null) mAutoComplete.CloseListWindow(); + //shouldCloseAutoComplete = true; } - - mAutoComplete?.UpdateAsyncInfo(); + + if (IsPrimaryTextCursor()) + mAutoComplete?.UpdateAsyncInfo(); return; } @@ -4176,6 +4252,7 @@ namespace IDE.ui (mData.mText[cursorTextPos - 1].mChar == '*') && (mData.mText[cursorTextPos].mChar == '\n')) { + CreateMultiCursorUndoBatch("SEWC.KeyChar(comment)"); InsertAtCursor("*"); let prevLineAndColumn = mEditWidget.mEditWidgetContent.CursorLineAndColumn; int column = GetLineEndColumn(prevLineAndColumn.mLine, false, true, true); @@ -4290,6 +4367,7 @@ namespace IDE.ui } else if ((keyChar == '{') || (keyChar == '(')) { + CreateMultiCursorUndoBatch("SEWC.KeyChar(blockSurround)"); UndoBatchStart undoBatchStart = new UndoBatchStart("blockSurround"); mData.mUndoManager.Add(undoBatchStart); @@ -4385,7 +4463,7 @@ namespace IDE.ui mIsInKeyChar = false; } - if ((keyChar == '\b') || (keyChar == '\r') || (keyChar >= (char8)32)) + if (IsPrimaryTextCursor() && ((keyChar == '\b') || (keyChar == '\r') || (keyChar >= (char8)32))) { bool isHighPri = (keyChar == '(') || (keyChar == '.'); bool needsFreshAutoComplete = ((isHighPri) /*|| (!mAsyncAutocomplete)*/ || (mAutoComplete == null) || (mAutoComplete.mAutoCompleteListWidget == null)); @@ -4414,7 +4492,7 @@ namespace IDE.ui } else if (mData.mCurTextVersionId != startRevision) { - if (mAutoComplete != null) + if (IsPrimaryTextCursor() && mAutoComplete != null) mAutoComplete.CloseListWindow(); } @@ -4487,6 +4565,7 @@ namespace IDE.ui mSelection.ValueRef.mStartPos = mSelection.Value.mEndPos - 1; if (mSelection.Value.mStartPos > 0) { + CreateMultiCursorUndoBatch("SEWC.KeyChar(case)"); DeleteSelection(); CursorToLineEnd(); } @@ -4509,6 +4588,7 @@ namespace IDE.ui int32 columnPos = (int32)(GetTabbedWidth(tabStartStr, 0) / mCharWidth + 0.001f); if (wantLineColumn > columnPos) { + CreateMultiCursorUndoBatch("SEWC.KeyChar(else)"); String insertStr = scope String(' ', wantLineColumn - columnPos); insertStr.Append("else"); @@ -4707,7 +4787,7 @@ namespace IDE.ui int prevTextLength = mData.mTextLength; base.KeyDown(keyCode, isRepeat); - if ((mAutoComplete != null) && + if ((IsPrimaryTextCursor()) && (mAutoComplete != null) && (keyCode != .Control) && (keyCode != .Shift)) { @@ -5020,6 +5100,7 @@ namespace IDE.ui menuItem = menu.AddItem("Cut|Ctrl+X"); menuItem.mOnMenuItemSelected.Add(new (menu) => { + SetPrimaryTextCursor(); CutText(); }); menuItem.SetDisabled(!hasSelection); @@ -5034,6 +5115,7 @@ namespace IDE.ui menuItem = menu.AddItem("Paste|Ctrl+V"); menuItem.mOnMenuItemSelected.Add(new (menu) => { + SetPrimaryTextCursor(); PasteText(); }); diff --git a/IDE/src/ui/SourceViewPanel.bf b/IDE/src/ui/SourceViewPanel.bf index 994a63dc..e0665998 100644 --- a/IDE/src/ui/SourceViewPanel.bf +++ b/IDE/src/ui/SourceViewPanel.bf @@ -1323,7 +1323,7 @@ namespace IDE.ui embedSource.mTypeName = new .(useTypeName); if (embedHasFocus) - embedSource.mCursorIdx = (.)embedSourceViewPanel.mEditWidget.mEditWidgetContent.CursorTextPos; + embedSource.mCursorIdx = (.)embedSourceViewPanel.mEditWidget.mEditWidgetContent.mTextCursors.Front.mCursorTextPos; else embedSource.mCursorIdx = -1; resolveParams.mEmitEmbeds.Add(embedSource); @@ -1385,7 +1385,7 @@ namespace IDE.ui { if ((useResolveType == .Autocomplete) || (useResolveType == .GetSymbolInfo) || (mIsClang)) { - resolveParams.mOverrideCursorPos = (.)mEditWidget.Content.CursorTextPos; + resolveParams.mOverrideCursorPos = (.)mEditWidget.Content.mTextCursors.Front.mCursorTextPos; /*if (useResolveType == .Autocomplete) resolveParams.mOverrideCursorPos--;*/ } @@ -2042,7 +2042,7 @@ namespace IDE.ui ProjectSource projectSource = FilteredProjectSource; - int cursorPos = mEditWidget.mEditWidgetContent.CursorTextPos; + int cursorPos = mEditWidget.mEditWidgetContent.mTextCursors.Front.mCursorTextPos; if ((resolveParams != null) && (resolveParams.mOverrideCursorPos != -1)) cursorPos = resolveParams.mOverrideCursorPos; @@ -5254,7 +5254,10 @@ namespace IDE.ui var sourceEditWidgetContent = (SourceEditWidgetContent)mEditWidget.Content; if (!sourceEditWidgetContent.CheckReadOnly()) + { + sourceEditWidgetContent.RemoveSecondaryTextCursors(); ShowSymbolReferenceHelper(SymbolReferenceHelper.Kind.Rename); + } } public void FindAllReferences()