diff --git a/TextEditor.cpp b/TextEditor.cpp index 8d16d619..b973efa5 100644 --- a/TextEditor.cpp +++ b/TextEditor.cpp @@ -37,20 +37,19 @@ TextEditor::TextEditor() , mColorizerEnabled(true) , mTextStart(20.0f) , mLeftMargin(10) - , mCursorPositionChanged(false) , mColorRangeMin(0) , mColorRangeMax(0) , mSelectionMode(SelectionMode::Normal) , mCheckComments(true) - , mLastClick(-1.0f) , mHandleKeyboardInputs(true) , mHandleMouseInputs(true) , mIgnoreImGuiChild(false) , mShowWhitespaces(true) , mShowShortTabGlyphs(false) , mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + , mLastClick(-1.0f) { - SetPalette(GetDarkPalette()); + SetPalette(GetMarianaPalette()); SetLanguageDefinition(LanguageDefinition::HLSL()); mLines.push_back(Line()); } @@ -112,9 +111,12 @@ std::string TextEditor::GetText(const Coordinates & aStart, const Coordinates & return result; } -TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates(int aCursor) const { - return SanitizeCoordinates(mState.mCursorPosition); + if (aCursor == -1) + return SanitizeCoordinates(mState.mCursors[mState.mCurrentCursor].mCursorPosition); + else + return SanitizeCoordinates(mState.mCursors[aCursor].mCursorPosition); } TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aValue) const @@ -228,31 +230,29 @@ void TextEditor::DeleteRange(const Coordinates & aStart, const Coordinates & aEn if (aEnd == aStart) return; - auto start = GetCharacterIndex(aStart); + auto start = GetCharacterIndexLeftSide(aStart); auto end = GetCharacterIndex(aEnd); if (aStart.mLine == aEnd.mLine) { - auto& line = mLines[aStart.mLine]; auto n = GetLineMaxColumn(aStart.mLine); if (aEnd.mColumn >= n) - line.erase(line.begin() + start, line.end()); + RemoveGlyphsFromLine(aStart.mLine, start); // from start to end of line else - line.erase(line.begin() + start, line.begin() + end); + RemoveGlyphsFromLine(aStart.mLine, start, end); } else { + RemoveGlyphsFromLine(aStart.mLine, start); // from start to end of line + RemoveGlyphsFromLine(aEnd.mLine, 0, end); auto& firstLine = mLines[aStart.mLine]; auto& lastLine = mLines[aEnd.mLine]; - firstLine.erase(firstLine.begin() + start, firstLine.end()); - lastLine.erase(lastLine.begin(), lastLine.begin() + end); - if (aStart.mLine < aEnd.mLine) - firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + AddGlyphsToLine(aStart.mLine, firstLine.size(), lastLine.begin(), lastLine.end()); if (aStart.mLine < aEnd.mLine) - RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + RemoveLines(aStart.mLine + 1, aEnd.mLine + 1); } mTextChanged = true; @@ -279,8 +279,8 @@ int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValu { auto& newLine = InsertLine(aWhere.mLine + 1); auto& line = mLines[aWhere.mLine]; - newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); - line.erase(line.begin() + cindex, line.end()); + AddGlyphsToLine(aWhere.mLine + 1, 0, line.begin() + cindex, line.end()); + RemoveGlyphsFromLine(aWhere.mLine, cindex); } else { @@ -297,7 +297,7 @@ int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValu auto& line = mLines[aWhere.mLine]; auto d = UTF8CharLength(*aValue); while (d-- > 0 && *aValue != '\0') - line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); + AddGlyphToLine(aWhere.mLine, cindex++, Glyph(*aValue++, PaletteIndex::Default)); aWhere.mColumn = GetCharacterColumn(aWhere.mLine, cindex); } @@ -322,10 +322,14 @@ void TextEditor::AddUndo(UndoRecord& aValue) ++mUndoIndex; } -TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode) const +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode, bool* isOverLineNumber) const { ImVec2 origin = ImGui::GetCursorScreenPos(); ImVec2 local(aPosition.x - origin.x + 3.0f, aPosition.y - origin.y); + + if (isOverLineNumber != nullptr) + *isOverLineNumber = local.x < mTextStart; + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); @@ -343,15 +347,17 @@ TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPositi int delta = 0; // First we find the hovered column coord. - while (mTextStart + columnX - (aInsertionMode ? 0.5f : 0.0f) * columnWidth < local.x && (size_t)columnIndex < line.size()) + for (size_t columnIndex = 0; columnIndex < line.size(); ++columnIndex) { - columnCoord += delta; + float columnWidth = 0.0f; + int delta = 0; + if (line[columnIndex].mChar == '\t') { float oldX = columnX; columnX = (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); columnWidth = columnX - oldX; - delta = columnCoord - (columnCoord / mTabSize) * mTabSize + mTabSize; + delta = mTabSize - (columnCoord % mTabSize); } else { @@ -365,7 +371,11 @@ TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPositi columnX += columnWidth; delta = 1; } - ++columnIndex; + + if (mTextStart + columnX - (aInsertionMode ? 0.5f : 0.0f) * columnWidth < local.x) + columnCoord += delta; + else + break; } // Then we reduce by 1 column coord if cursor is on the left side of the hovered column. @@ -383,7 +393,7 @@ TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates & aFrom) con return at; auto& line = mLines[at.mLine]; - auto cindex = GetCharacterIndex(at); + auto cindex = GetCharacterIndexLeftSide(at); if (cindex >= (int)line.size()) return at; @@ -493,6 +503,34 @@ TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates & aFrom) cons return at; } +int TextEditor::GetCharacterIndexLeftSide(const Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine >= mLines.size()) + return -1; + + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + int tabCoordsLeft = 0; + + for (; i < line.size() && c < aCoordinates.mColumn;) + { + if (line[i].mChar == '\t') + { + if (tabCoordsLeft == 0) + tabCoordsLeft = mTabSize - (c % mTabSize); + if (tabCoordsLeft > 0) + tabCoordsLeft--; + c++; + } + else + ++c; + if (tabCoordsLeft == 0) + i += UTF8CharLength(line[i].mChar); + } + return i; +} + int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const { if (aCoordinates.mLine >= mLines.size()) @@ -575,7 +613,7 @@ bool TextEditor::IsOnWordBoundary(const Coordinates & aAt) const return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); } -void TextEditor::RemoveLine(int aStart, int aEnd) +void TextEditor::RemoveLines(int aStart, int aEnd) { assert(!mReadOnly); assert(aEnd >= aStart); @@ -604,6 +642,8 @@ void TextEditor::RemoveLine(int aStart, int aEnd) assert(!mLines.empty()); mTextChanged = true; + + OnLinesDeleted(aStart, aEnd); } void TextEditor::RemoveLine(int aIndex) @@ -634,6 +674,115 @@ void TextEditor::RemoveLine(int aIndex) assert(!mLines.empty()); mTextChanged = true; + + OnLineDeleted(aIndex); +} + +void TextEditor::RemoveCurrentLines() +{ + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + for (int c = mState.mCurrentCursor; c > -1; c--) + { + u.mRemoved.push_back({ GetSelectedText(c), mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd }); + DeleteSelection(c); + } + } + + for (int c = mState.mCurrentCursor; c > -1; c--) + { + int currentLine = mState.mCursors[c].mCursorPosition.mLine; + int nextLine = currentLine + 1; + int prevLine = currentLine - 1; + + Coordinates toDeleteStart, toDeleteEnd; + if (mLines.size() > nextLine) // next line exists + { + toDeleteStart = Coordinates(currentLine, 0); + toDeleteEnd = Coordinates(nextLine, 0); + mState.mCursors[c].mCursorPosition.mColumn = 0; + } + else if (prevLine > -1) // previous line exists + { + toDeleteStart = Coordinates(prevLine, GetLineMaxColumn(prevLine)); + toDeleteEnd = Coordinates(currentLine, GetLineMaxColumn(currentLine)); + mState.mCursors[c].mCursorPosition.mLine--; + mState.mCursors[c].mCursorPosition.mColumn = 0; + } + else + { + toDeleteStart = Coordinates(currentLine, 0); + toDeleteEnd = Coordinates(currentLine, GetLineMaxColumn(currentLine)); + mState.mCursors[c].mCursorPosition.mColumn = 0; + } + + u.mRemoved.push_back({ GetText(toDeleteStart, toDeleteEnd), toDeleteStart, toDeleteEnd }); + + if (toDeleteStart.mLine != toDeleteEnd.mLine) + RemoveLine(currentLine); + else + DeleteRange(toDeleteStart, toDeleteEnd); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::OnLineChanged(bool aBeforeChange, int aLine, int aColumn, int aCharCount, bool aDeleted) +{ + static std::unordered_map cursorCharIndices; + if (aBeforeChange) + { + cursorCharIndices.clear(); + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mCursorPosition.mLine == aLine) + { + if (mState.mCursors[c].mCursorPosition.mColumn > aColumn) + { + cursorCharIndices[c] = GetCharacterIndex({ aLine, mState.mCursors[c].mCursorPosition.mColumn }); + cursorCharIndices[c] += aDeleted ? -aCharCount : aCharCount; + } + } + } + } + else + { + for (auto& item : cursorCharIndices) + SetCursorPosition({ aLine, GetCharacterColumn(aLine, item.second) }, item.first); + } +} + +void TextEditor::RemoveGlyphsFromLine(int aLine, int aStartChar, int aEndChar) +{ + int column = GetCharacterColumn(aLine, aStartChar); + int deltaX = GetCharacterColumn(aLine, aEndChar) - column; + auto& line = mLines[aLine]; + OnLineChanged(true, aLine, column, aEndChar - aStartChar, true); + line.erase(line.begin() + aStartChar, aEndChar == -1 ? line.end() : line.begin() + aEndChar); + OnLineChanged(false, aLine, column, aEndChar - aStartChar, true); +} + +void TextEditor::AddGlyphsToLine(int aLine, int aTargetIndex, Line::iterator aSourceStart, Line::iterator aSourceEnd) +{ + int targetColumn = GetCharacterColumn(aLine, aTargetIndex); + int charsInserted = std::distance(aSourceStart, aSourceEnd); + auto& line = mLines[aLine]; + OnLineChanged(true, aLine, targetColumn, charsInserted, false); + line.insert(line.begin() + aTargetIndex, aSourceStart, aSourceEnd); + OnLineChanged(false, aLine, targetColumn, charsInserted, false); +} + +void TextEditor::AddGlyphToLine(int aLine, int aTargetIndex, Glyph aGlyph) +{ + int targetColumn = GetCharacterColumn(aLine, aTargetIndex); + auto& line = mLines[aLine]; + OnLineChanged(true, aLine, targetColumn, 1, false); + line.insert(line.begin() + aTargetIndex, aGlyph); + OnLineChanged(false, aLine, targetColumn, 1, false); } TextEditor::Line& TextEditor::InsertLine(int aIndex) @@ -652,6 +801,8 @@ TextEditor::Line& TextEditor::InsertLine(int aIndex) btmp.insert(i >= aIndex ? i + 1 : i); mBreakpoints = std::move(btmp); + OnLineAdded(aIndex); + return result; } @@ -700,7 +851,7 @@ ImU32 TextEditor::GetGlyphColor(const Glyph & aGlyph) const void TextEditor::HandleKeyboardInputs() { - if (ImGui::IsWindowFocused()) + if (ImGui::IsWindowFocused() || ImGui::IsRootWindowFocused()) { if (ImGui::IsWindowHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); @@ -751,10 +902,12 @@ void TextEditor::HandleKeyboardInputs() MoveHome(shift); else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End))) MoveEnd(shift); - else if (!IsReadOnly() && !alt && !ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) - Delete(); - else if (!IsReadOnly() && !alt && !ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) - Backspace(); + else if (!IsReadOnly() && !alt && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) + Delete(ctrl); + else if (!IsReadOnly() && !alt && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) + Backspace(ctrl); + else if (!IsReadOnly() && !alt && ctrl && shift && !super && ImGui::IsKeyPressed('K')) + RemoveCurrentLines(); else if (!alt && !ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) mOverwrite ^= true; else if (isCtrlOnly && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) @@ -797,9 +950,9 @@ void TextEditor::HandleMouseInputs() if (ImGui::IsWindowHovered()) { + auto click = ImGui::IsMouseClicked(0); if (!shift && !alt) { - auto click = ImGui::IsMouseClicked(0); auto doubleClick = ImGui::IsMouseDoubleClicked(0); auto t = ImGui::GetTime(); auto tripleClick = click && !doubleClick && (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); @@ -810,12 +963,14 @@ void TextEditor::HandleMouseInputs() if (tripleClick) { - if (!ctrl) - { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); - mSelectionMode = SelectionMode::Line; - SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); - } + if (ctrl) + mState.AddCursor(); + else + mState.mCurrentCursor = 0; + + mState.mCursors[mState.mCurrentCursor].mCursorPosition = mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd, mSelectionMode); mLastClick = -1.0f; } @@ -826,15 +981,19 @@ void TextEditor::HandleMouseInputs() else if (doubleClick) { - if (!ctrl) - { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); - if (mSelectionMode == SelectionMode::Line) - mSelectionMode = SelectionMode::Normal; - else - mSelectionMode = SelectionMode::Word; - SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); - } + if (ctrl) + mState.AddCursor(); + else + mState.mCurrentCursor = 0; + + mState.mCursors[mState.mCurrentCursor].mCursorPosition = mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = FindWordStart(mState.mCursors[mState.mCurrentCursor].mCursorPosition); + mState.mCursors[mState.mCurrentCursor].mCursorPosition = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = FindWordEnd(mState.mCursors[mState.mCurrentCursor].mCursorPosition); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd, mSelectionMode); mLastClick = (float)ImGui::GetTime(); } @@ -844,39 +1003,78 @@ void TextEditor::HandleMouseInputs() */ else if (click) { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite)); if (ctrl) + mState.AddCursor(); + else + mState.mCurrentCursor = 0; + + bool isOverLineNumber; + mState.mCursors[mState.mCurrentCursor].mCursorPosition = mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite, &isOverLineNumber); + if (isOverLineNumber) + mSelectionMode = SelectionMode::Line; + else if (ctrl) mSelectionMode = SelectionMode::Word; else mSelectionMode = SelectionMode::Normal; - SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd, mSelectionMode, -1, ctrl); mLastClick = (float)ImGui::GetTime(); } // Mouse left button dragging (=> update selection) else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) { + mDraggingSelection = true; io.WantCaptureMouse = true; - mState.mCursorPosition = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite)); - SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + mState.mCursors[mState.mCurrentCursor].mCursorPosition = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite); + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd, mSelectionMode); + } + else if (ImGui::IsMouseReleased(0)) + { + mDraggingSelection = false; + + // sort from cursors from top to bottom + std::sort(mState.mCursors.begin(), mState.mCursors.begin() + (mState.mCurrentCursor + 1), [](const Cursor& a, const Cursor& b) -> bool + { + return a.mSelectionStart < b.mSelectionStart; + }); + MergeCursorsIfPossible(); + } + } + else if (shift) + { + if (click) + { + Coordinates oldCursorPosition = mState.mCursors[mState.mCurrentCursor].mCursorPosition; + Coordinates newSelection = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite); + if (newSelection > mState.mCursors[mState.mCurrentCursor].mCursorPosition) + SetSelectionEnd(newSelection); + else + SetSelectionStart(newSelection); + mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = mState.mCursors[mState.mCurrentCursor].mSelectionEnd; + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mSelectionStart; + mState.mCursors[mState.mCurrentCursor].mCursorPosition = newSelection; + mState.mCursors[mState.mCurrentCursor].mCursorPositionChanged = oldCursorPosition != newSelection; } } } } -void TextEditor::Render() +void TextEditor::UpdatePalette() { - /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ - const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x; - mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); - /* Update palette with the current alpha from style */ for (int i = 0; i < (int)PaletteIndex::Max; ++i) { - auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + auto color = U32ColorToVec4(mPaletteBase[i]); color.w *= ImGui::GetStyle().Alpha; mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); } +} + +void TextEditor::Render() +{ + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x; + mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); assert(mLineBuffer.empty()); @@ -919,23 +1117,26 @@ void TextEditor::Render() Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); // Draw selection for the current line - float sstart = -1.0f; - float ssend = -1.0f; + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + float sstart = -1.0f; + float ssend = -1.0f; - assert(mState.mSelectionStart <= mState.mSelectionEnd); - if (mState.mSelectionStart <= lineEndCoord) - sstart = mState.mSelectionStart > lineStartCoord ? TextDistanceToLineStart(mState.mSelectionStart) : 0.0f; - if (mState.mSelectionEnd > lineStartCoord) - ssend = TextDistanceToLineStart(mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord); + assert(mState.mCursors[c].mSelectionStart <= mState.mCursors[c].mSelectionEnd); + if (mState.mCursors[c].mSelectionStart <= lineEndCoord) + sstart = mState.mCursors[c].mSelectionStart > lineStartCoord ? TextDistanceToLineStart(mState.mCursors[c].mSelectionStart) : 0.0f; + if (mState.mCursors[c].mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart(mState.mCursors[c].mSelectionEnd < lineEndCoord ? mState.mCursors[c].mSelectionEnd : lineEndCoord); - if (mState.mSelectionEnd.mLine > lineNo) - ssend += mCharAdvance.x; + if (mState.mCursors[c].mSelectionEnd.mLine > lineNo) + ssend += mCharAdvance.x; - if (sstart != -1 && ssend != -1 && sstart < ssend) - { - ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); - ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, lineStartScreenPos.y + mCharAdvance.y); - drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]); + if (sstart != -1 && ssend != -1 && sstart < ssend) + { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]); + } } // Draw breakpoints @@ -974,28 +1175,24 @@ void TextEditor::Render() auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], buf); - if (mState.mCursorPosition.mLine == lineNo) + std::vector cursorCoordsInThisLine; + for (int c = 0; c <= mState.mCurrentCursor; c++) { - auto focused = ImGui::IsWindowFocused(); - - // Highlight the current line (where the cursor is) - if (!HasSelection()) - { - auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y); - drawList->AddRectFilled(start, end, mPalette[(int)(focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]); - drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); - } + if (mState.mCursors[c].mCursorPosition.mLine == lineNo) + cursorCoordsInThisLine.push_back(mState.mCursors[c].mCursorPosition); + } + if (cursorCoordsInThisLine.size() > 0) + { + auto focused = ImGui::IsWindowFocused() || ImGui::IsRootWindowFocused(); - // Render the cursor + // Render the cursors if (focused) { - auto timeEnd = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - auto elapsed = timeEnd - mStartTime; - if (elapsed > 400) + for (const auto& cursorCoords : cursorCoordsInThisLine) { float width = 1.0f; - auto cindex = GetCharacterIndex(mState.mCursorPosition); - float cx = TextDistanceToLineStart(mState.mCursorPosition); + auto cindex = GetCharacterIndex(cursorCoords); + float cx = TextDistanceToLineStart(cursorCoords); if (mOverwrite && cindex < (int)line.size()) { @@ -1016,8 +1213,6 @@ void TextEditor::Render() ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); ImVec2 cend(textScreenPos.x + cx + width, lineStartScreenPos.y + mCharAdvance.y); drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); - if (elapsed > 800) - mStartTime = timeEnd; } } } @@ -1076,9 +1271,9 @@ void TextEditor::Render() p4 = ImVec2(x2 - s * 0.2f, y + s * 0.2f); } - drawList->AddLine(p1, p2, 0x90909090); - drawList->AddLine(p2, p3, 0x90909090); - drawList->AddLine(p2, p4, 0x90909090); + drawList->AddLine(p1, p2, mPalette[(int)PaletteIndex::ControlCharacter]); + drawList->AddLine(p2, p3, mPalette[(int)PaletteIndex::ControlCharacter]); + drawList->AddLine(p2, p4, mPalette[(int)PaletteIndex::ControlCharacter]); } } else if (glyph.mChar == ' ') @@ -1113,7 +1308,7 @@ void TextEditor::Render() } // Draw a tooltip on known identifiers/preprocessor symbols - if (ImGui::IsMousePosValid()) + if (ImGui::IsMousePosValid() && ImGui::IsWindowHovered()) { auto mpos = ImGui::GetMousePos(); ImVec2 origin = ImGui::GetCursorScreenPos(); @@ -1122,7 +1317,7 @@ void TextEditor::Render() if (local.x >= mTextStart) { auto pos = ScreenPosToCoordinates(mpos); - printf("Coord(%d, %d)\n", pos.mLine, pos.mColumn); + //printf("Coord(%d, %d)\n", pos.mLine, pos.mColumn); auto id = GetWordAt(pos); if (!id.empty()) { @@ -1148,27 +1343,34 @@ void TextEditor::Render() } } - + ImGui::SetCursorPos(ImVec2(0, 0)); ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y)); if (mScrollToCursor) { EnsureCursorVisible(); - ImGui::SetWindowFocus(); mScrollToCursor = false; } } void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) { + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mCursorPositionChanged) + OnCursorPositionChanged(c); + mState.mCursors[c].mCursorPositionChanged = false; + } + mWithinRender = true; mTextChanged = false; - mCursorPositionChanged = false; + + UpdatePalette(); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); if (!mIgnoreImGuiChild) - ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoMove); + ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNavInputs); if (mHandleKeyboardInputs) { @@ -1194,7 +1396,7 @@ void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) mWithinRender = false; } -void TextEditor::SetText(const std::string & aText) +void TextEditor::SetText(const std::string& aText) { mLines.clear(); mLines.emplace_back(Line()); @@ -1221,7 +1423,7 @@ void TextEditor::SetText(const std::string & aText) Colorize(); } -void TextEditor::SetTextLines(const std::vector & aLines) +void TextEditor::SetTextLines(const std::vector& aLines) { mLines.clear(); @@ -1235,7 +1437,7 @@ void TextEditor::SetTextLines(const std::vector & aLines) for (size_t i = 0; i < aLines.size(); ++i) { - const std::string & aLine = aLines[i]; + const std::string& aLine = aLines[i]; mLines[i].reserve(aLine.size()); for (size_t j = 0; j < aLine.size(); ++j) @@ -1262,166 +1464,178 @@ void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) if (HasSelection()) { - if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) + for (int c = mState.mCurrentCursor; c > -1; c--) { + if (aChar == '\t' && mState.mCursors[c].mSelectionStart.mLine != mState.mCursors[c].mSelectionEnd.mLine) + { + auto start = mState.mCursors[c].mSelectionStart; + auto end = mState.mCursors[c].mSelectionEnd; + auto originalEnd = end; - auto start = mState.mSelectionStart; - auto end = mState.mSelectionEnd; - auto originalEnd = end; - - if (start > end) - std::swap(start, end); - start.mColumn = 0; - // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; - if (end.mColumn == 0 && end.mLine > 0) - --end.mLine; - if (end.mLine >= (int)mLines.size()) - end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; - end.mColumn = GetLineMaxColumn(end.mLine); + if (start > end) + std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) + --end.mLine; + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); - //if (end.mColumn >= GetLineMaxColumn(end.mLine)) - // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + //if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; - u.mRemovedStart = start; - u.mRemovedEnd = end; - u.mRemoved = GetText(start, end); + u.mRemoved.push_back({ GetText(start, end) , start, end }); - bool modified = false; + bool modified = false; - for (int i = start.mLine; i <= end.mLine; i++) - { - auto& line = mLines[i]; - if (aShift) + for (int i = start.mLine; i <= end.mLine; i++) { - if (!line.empty()) + auto& line = mLines[i]; + if (aShift) { - if (line.front().mChar == '\t') - { - line.erase(line.begin()); - modified = true; - } - else + if (!line.empty()) + { - for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + if (line.front().mChar == '\t') { - line.erase(line.begin()); + RemoveGlyphsFromLine(i, 0, 1); modified = true; } + else + { + for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + { + RemoveGlyphsFromLine(i, 0, 1); + modified = true; + } + } } } + else + { + AddGlyphToLine(i, 0, Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } } - else - { - line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); - modified = true; - } - } - if (modified) - { - start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); - Coordinates rangeEnd; - if (originalEnd.mColumn != 0) - { - end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); - rangeEnd = end; - u.mAdded = GetText(start, end); - } - else + if (modified) { - end = Coordinates(originalEnd.mLine, 0); - rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); - u.mAdded = GetText(start, rangeEnd); - } + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + std::string addedText; + if (originalEnd.mColumn != 0) + { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + addedText = GetText(start, end); + } + else + { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + addedText = GetText(start, rangeEnd); + } - u.mAddedStart = start; - u.mAddedEnd = rangeEnd; - u.mAfter = mState; + u.mAdded.push_back({ addedText , start, rangeEnd }); + u.mAfter = mState; - mState.mSelectionStart = start; - mState.mSelectionEnd = end; - AddUndo(u); + mState.mCursors[c].mSelectionStart = start; + mState.mCursors[c].mSelectionEnd = end; + AddUndo(u); - mTextChanged = true; + mTextChanged = true; - EnsureCursorVisible(); - } + EnsureCursorVisible(); + } - return; - } // c == '\t' - else - { - u.mRemoved = GetSelectedText(); - u.mRemovedStart = mState.mSelectionStart; - u.mRemovedEnd = mState.mSelectionEnd; - DeleteSelection(); + return; + } // c == '\t' + else + { + u.mRemoved.push_back({ GetSelectedText(), mState.mCursors[c].mSelectionStart, mState.mCursors[mState.mCurrentCursor].mSelectionEnd }); + DeleteSelection(c); + } } } // HasSelection - auto coord = GetActualCursorCoordinates(); - u.mAddedStart = coord; - - assert(!mLines.empty()); - - if (aChar == '\n') + std::vector coords; + for (int c = 0; c <= mState.mCurrentCursor; c++) // order important here for typing tabs in the same line at the same time { - InsertLine(coord.mLine + 1); - auto& line = mLines[coord.mLine]; - auto& newLine = mLines[coord.mLine + 1]; + auto coord = GetActualCursorCoordinates(c); + coords.push_back(coord); + Selection added, removed; + added.mStart = coord; - if (mLanguageDefinition.mAutoIndentation) - for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) - newLine.push_back(line[it]); + assert(!mLines.empty()); - const size_t whitespaceSize = newLine.size(); - auto cindex = GetCharacterIndex(coord); - newLine.insert(newLine.end(), line.begin() + cindex, line.end()); - line.erase(line.begin() + cindex, line.begin() + line.size()); - SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize))); - u.mAdded = (char)aChar; - } - else - { - char buf[7]; - int e = ImTextCharToUtf8(buf, 7, aChar); - if (e > 0) + if (aChar == '\n') { - buf[e] = '\0'; + InsertLine(coord.mLine + 1); auto& line = mLines[coord.mLine]; - auto cindex = GetCharacterIndex(coord); + auto& newLine = mLines[coord.mLine + 1]; - if (mOverwrite && cindex < (int)line.size()) - { - auto d = UTF8CharLength(line[cindex].mChar); + added.mText = ""; + added.mText += (char)aChar; + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) + { + newLine.push_back(line[it]); + added.mText += line[it].mChar; + } - u.mRemovedStart = mState.mCursorPosition; - u.mRemovedEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + AddGlyphsToLine(coord.mLine + 1, newLine.size(), line.begin() + cindex, line.end()); + RemoveGlyphsFromLine(coord.mLine, cindex); + SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize)), c); + } + else + { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) + { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); - while (d-- > 0 && cindex < (int)line.size()) + if (mOverwrite && cindex < (int)line.size()) { - u.mRemoved += line[cindex].mChar; - line.erase(line.begin() + cindex); + auto d = UTF8CharLength(line[cindex].mChar); + + removed.mStart = mState.mCursors[c].mCursorPosition; + removed.mEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) + { + removed.mText += line[cindex].mChar; + RemoveGlyphsFromLine(coord.mLine, cindex, cindex + 1); + } } - } - for (auto p = buf; *p != '\0'; p++, ++cindex) - line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); - u.mAdded = buf; + for (auto p = buf; *p != '\0'; p++, ++cindex) + AddGlyphToLine(coord.mLine, cindex, Glyph(*p, PaletteIndex::Default)); + added.mText = buf; - SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex)), c); + } + else + return; } - else - return; - } - mTextChanged = true; + mTextChanged = true; - u.mAddedEnd = GetActualCursorCoordinates(); - u.mAfter = mState; + added.mEnd = GetActualCursorCoordinates(c); + u.mAdded.push_back(added); + u.mRemoved.push_back(removed); + u.mAfter = mState; + } AddUndo(u); - Colorize(coord.mLine - 1, 3); + for (const auto& coord : coords) + Colorize(coord.mLine - 1, 3); EnsureCursorVisible(); } @@ -1430,71 +1644,92 @@ void TextEditor::SetReadOnly(bool aValue) mReadOnly = aValue; } +void TextEditor::OnCursorPositionChanged(int aCursor) +{ + if (mDraggingSelection) + return; + + // sort from cursors from top to bottom + std::sort(mState.mCursors.begin(), mState.mCursors.begin() + (mState.mCurrentCursor + 1), [](const Cursor& a, const Cursor& b) -> bool + { + return a.mSelectionStart < b.mSelectionStart; + }); + MergeCursorsIfPossible(); +} + void TextEditor::SetColorizerEnable(bool aValue) { mColorizerEnabled = aValue; } -void TextEditor::SetCursorPosition(const Coordinates & aPosition) +void TextEditor::SetCursorPosition(const Coordinates& aPosition, int aCursor) { - if (mState.mCursorPosition != aPosition) + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + + if (mState.mCursors[aCursor].mCursorPosition != aPosition) { - mState.mCursorPosition = aPosition; - mCursorPositionChanged = true; + mState.mCursors[aCursor].mCursorPosition = aPosition; + mState.mCursors[aCursor].mCursorPositionChanged = true; EnsureCursorVisible(); } } -void TextEditor::SetSelectionStart(const Coordinates & aPosition) +void TextEditor::SetSelectionStart(const Coordinates& aPosition, int aCursor) { - mState.mSelectionStart = SanitizeCoordinates(aPosition); - if (mState.mSelectionStart > mState.mSelectionEnd) - std::swap(mState.mSelectionStart, mState.mSelectionEnd); + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + + mState.mCursors[aCursor].mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mCursors[aCursor].mSelectionStart > mState.mCursors[aCursor].mSelectionEnd) + std::swap(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionEnd); } -void TextEditor::SetSelectionEnd(const Coordinates & aPosition) +void TextEditor::SetSelectionEnd(const Coordinates& aPosition, int aCursor) { - mState.mSelectionEnd = SanitizeCoordinates(aPosition); - if (mState.mSelectionStart > mState.mSelectionEnd) - std::swap(mState.mSelectionStart, mState.mSelectionEnd); + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + + mState.mCursors[aCursor].mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mCursors[aCursor].mSelectionStart > mState.mCursors[aCursor].mSelectionEnd) + std::swap(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionEnd); } -void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aEnd, SelectionMode aMode) +void TextEditor::SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode, int aCursor, bool isSpawningNewCursor) { - auto oldSelStart = mState.mSelectionStart; - auto oldSelEnd = mState.mSelectionEnd; + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + + auto oldSelStart = mState.mCursors[aCursor].mSelectionStart; + auto oldSelEnd = mState.mCursors[aCursor].mSelectionEnd; - mState.mSelectionStart = SanitizeCoordinates(aStart); - mState.mSelectionEnd = SanitizeCoordinates(aEnd); - if (mState.mSelectionStart > mState.mSelectionEnd) - std::swap(mState.mSelectionStart, mState.mSelectionEnd); + mState.mCursors[aCursor].mSelectionStart = SanitizeCoordinates(aStart); + mState.mCursors[aCursor].mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mCursors[aCursor].mSelectionStart > mState.mCursors[aCursor].mSelectionEnd) + std::swap(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionEnd); switch (aMode) { case TextEditor::SelectionMode::Normal: - break; case TextEditor::SelectionMode::Word: - { - mState.mSelectionStart = FindWordStart(mState.mSelectionStart); - if (!IsOnWordBoundary(mState.mSelectionEnd)) - mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); break; - } case TextEditor::SelectionMode::Line: { - const auto lineNo = mState.mSelectionEnd.mLine; + const auto lineNo = mState.mCursors[aCursor].mSelectionEnd.mLine; const auto lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; - mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); - mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + mState.mCursors[aCursor].mSelectionStart = Coordinates(mState.mCursors[aCursor].mSelectionStart.mLine, 0); + mState.mCursors[aCursor].mSelectionEnd = mLines.size() > lineNo + 1 ? Coordinates(lineNo + 1, 0) : Coordinates(lineNo, GetLineMaxColumn(lineNo)); + mState.mCursors[aCursor].mCursorPosition = mState.mCursors[aCursor].mSelectionEnd; break; } default: break; } - if (mState.mSelectionStart != oldSelStart || - mState.mSelectionEnd != oldSelEnd) - mCursorPositionChanged = true; + if (mState.mCursors[aCursor].mSelectionStart != oldSelStart || + mState.mCursors[aCursor].mSelectionEnd != oldSelEnd) + if (!isSpawningNewCursor) + mState.mCursors[aCursor].mCursorPositionChanged = true; } void TextEditor::SetTabSize(int aValue) @@ -1502,93 +1737,105 @@ void TextEditor::SetTabSize(int aValue) mTabSize = std::max(0, std::min(32, aValue)); } -void TextEditor::InsertText(const std::string & aValue) +void TextEditor::InsertText(const std::string& aValue, int aCursor) { - InsertText(aValue.c_str()); + InsertText(aValue.c_str(), aCursor); } -void TextEditor::InsertText(const char * aValue) +void TextEditor::InsertText(const char* aValue, int aCursor) { if (aValue == nullptr) return; + if (aCursor == -1) + aCursor = mState.mCurrentCursor; - auto pos = GetActualCursorCoordinates(); - auto start = std::min(pos, mState.mSelectionStart); + auto pos = GetActualCursorCoordinates(aCursor); + auto start = std::min(pos, mState.mCursors[aCursor].mSelectionStart); int totalLines = pos.mLine - start.mLine; totalLines += InsertTextAt(pos, aValue); - SetSelection(pos, pos); - SetCursorPosition(pos); + SetSelection(pos, pos, SelectionMode::Normal, aCursor); + SetCursorPosition(pos, aCursor); Colorize(start.mLine - 1, totalLines + 2); } -void TextEditor::DeleteSelection() +void TextEditor::DeleteSelection(int aCursor) { - assert(mState.mSelectionEnd >= mState.mSelectionStart); + if (aCursor == -1) + aCursor = mState.mCurrentCursor; - if (mState.mSelectionEnd == mState.mSelectionStart) + assert(mState.mCursors[aCursor].mSelectionEnd >= mState.mCursors[aCursor].mSelectionStart); + + if (mState.mCursors[aCursor].mSelectionEnd == mState.mCursors[aCursor].mSelectionStart) return; - DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + DeleteRange(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionEnd); - SetSelection(mState.mSelectionStart, mState.mSelectionStart); - SetCursorPosition(mState.mSelectionStart); - Colorize(mState.mSelectionStart.mLine, 1); + SetSelection(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionStart, SelectionMode::Normal, aCursor); + SetCursorPosition(mState.mCursors[aCursor].mSelectionStart, aCursor); + mState.mCursors[aCursor].mInteractiveStart = mState.mCursors[aCursor].mSelectionStart; + mState.mCursors[aCursor].mInteractiveEnd = mState.mCursors[aCursor].mSelectionEnd; + Colorize(mState.mCursors[aCursor].mSelectionStart.mLine, 1); } void TextEditor::MoveUp(int aAmount, bool aSelect) { - auto oldPos = mState.mCursorPosition; - mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount); - if (oldPos != mState.mCursorPosition) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (aSelect) + auto oldPos = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mCursorPosition.mLine = std::max(0, mState.mCursors[c].mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursors[c].mCursorPosition) { - if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; - else if (oldPos == mInteractiveEnd) - mInteractiveEnd = mState.mCursorPosition; - else + if (aSelect) { - mInteractiveStart = mState.mCursorPosition; - mInteractiveEnd = oldPos; + if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mInteractiveEnd = oldPos; + } } + else + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, SelectionMode::Normal, c); } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd); - - EnsureCursorVisible(); } + EnsureCursorVisible(); } void TextEditor::MoveDown(int aAmount, bool aSelect) { - assert(mState.mCursorPosition.mColumn >= 0); - auto oldPos = mState.mCursorPosition; - mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); - - if (mState.mCursorPosition != oldPos) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (aSelect) + assert(mState.mCursors[c].mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursors[c].mCursorPosition.mLine + aAmount)); + + if (mState.mCursors[c].mCursorPosition != oldPos) { - if (oldPos == mInteractiveEnd) - mInteractiveEnd = mState.mCursorPosition; - else if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; - else + if (aSelect) { - mInteractiveStart = oldPos; - mInteractiveEnd = mState.mCursorPosition; + if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + else if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = oldPos; + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + } } - } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd); + else + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, SelectionMode::Normal, c); - EnsureCursorVisible(); + } } + EnsureCursorVisible(); } static bool IsUTFSequence(char c) @@ -1601,202 +1848,228 @@ void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) if (mLines.empty()) return; - auto oldPos = mState.mCursorPosition; - mState.mCursorPosition = GetActualCursorCoordinates(); - auto line = mState.mCursorPosition.mLine; - auto cindex = GetCharacterIndex(mState.mCursorPosition); - - while (aAmount-- > 0) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (cindex == 0) + int amount = aAmount; + auto oldPos = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mCursorPosition = GetActualCursorCoordinates(c); + auto line = mState.mCursors[c].mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursors[c].mCursorPosition); + + while (amount-- > 0) { - if (line > 0) + if (cindex == 0) { - --line; - if ((int)mLines.size() > line) - cindex = (int)mLines[line].size(); - else - cindex = 0; + if (line > 0) + { + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; + } } - } - else - { - --cindex; - if (cindex > 0) + else { - if ((int)mLines.size() > line) + --cindex; + if (cindex > 0) { - while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) - --cindex; + if ((int)mLines.size() > line) + { + while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) + --cindex; + } } } - } - mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); - if (aWordMode) - { - mState.mCursorPosition = FindWordStart(mState.mCursorPosition); - cindex = GetCharacterIndex(mState.mCursorPosition); + mState.mCursors[c].mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) + { + mState.mCursors[c].mCursorPosition = FindWordStart(mState.mCursors[c].mCursorPosition); + cindex = GetCharacterIndex(mState.mCursors[c].mCursorPosition); + } } - } - mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + mState.mCursors[c].mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + std::cout << "changed from " << oldPos.mColumn << " to " << mState.mCursors[c].mCursorPosition.mColumn << std::endl; - assert(mState.mCursorPosition.mColumn >= 0); - if (aSelect) - { - if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; - else if (oldPos == mInteractiveEnd) - mInteractiveEnd = mState.mCursorPosition; + assert(mState.mCursors[c].mCursorPosition.mColumn >= 0); + if (aSelect) + { + if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mInteractiveEnd = oldPos; + } + } else { - mInteractiveStart = mState.mCursorPosition; - mInteractiveEnd = oldPos; + if (HasSelection() && !aWordMode) + mState.mCursors[c].mCursorPosition = mState.mCursors[c].mInteractiveStart; + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; } + std::cout << "Setting selection for " << c << std::endl; + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal, c); } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); - EnsureCursorVisible(); } void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) { - auto oldPos = mState.mCursorPosition; - - if (mLines.empty() || oldPos.mLine >= mLines.size()) + if (mLines.empty()) return; - auto cindex = GetCharacterIndex(mState.mCursorPosition); - while (aAmount-- > 0) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - auto lindex = mState.mCursorPosition.mLine; - auto& line = mLines[lindex]; + auto oldPos = mState.mCursors[c].mCursorPosition; + if (oldPos.mLine >= mLines.size()) + continue; - if (cindex >= line.size()) + int amount = aAmount; + auto cindex = GetCharacterIndex(mState.mCursors[c].mCursorPosition); + while (amount-- > 0) { - if (mState.mCursorPosition.mLine < mLines.size() - 1) + auto lindex = mState.mCursors[c].mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) { - mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); - mState.mCursorPosition.mColumn = 0; + if (mState.mCursors[c].mCursorPosition.mLine < mLines.size() - 1) + { + mState.mCursors[c].mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursors[c].mCursorPosition.mLine + 1)); + mState.mCursors[c].mCursorPosition.mColumn = 0; + } + else + return; } else - return; + { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursors[c].mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursors[c].mCursorPosition = FindWordEnd(mState.mCursors[c].mCursorPosition); + } } - else + + if (aSelect) { - cindex += UTF8CharLength(line[cindex].mChar); - mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); - if (aWordMode) - mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = SanitizeCoordinates(mState.mCursors[c].mCursorPosition); + else if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = oldPos; + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + } } - } - - if (aSelect) - { - if (oldPos == mInteractiveEnd) - mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); - else if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; else { - mInteractiveStart = oldPos; - mInteractiveEnd = mState.mCursorPosition; + if (HasSelection() && !aWordMode) + mState.mCursors[c].mCursorPosition = mState.mCursors[c].mInteractiveEnd; + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; } + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal, c); } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); - EnsureCursorVisible(); } void TextEditor::MoveTop(bool aSelect) { - auto oldPos = mState.mCursorPosition; + mState.mCurrentCursor = 0; + auto oldPos = mState.mCursors[mState.mCurrentCursor].mCursorPosition; SetCursorPosition(Coordinates(0, 0)); - if (mState.mCursorPosition != oldPos) + if (mState.mCursors[mState.mCurrentCursor].mCursorPosition != oldPos) { if (aSelect) { - mInteractiveEnd = oldPos; - mInteractiveStart = mState.mCursorPosition; + mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = oldPos; + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mCursorPosition; } else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd); + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = mState.mCursors[mState.mCurrentCursor].mCursorPosition; + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd); } } void TextEditor::TextEditor::MoveBottom(bool aSelect) { + mState.mCurrentCursor = 0; auto oldPos = GetCursorPosition(); auto newPos = Coordinates((int)mLines.size() - 1, 0); SetCursorPosition(newPos); if (aSelect) { - mInteractiveStart = oldPos; - mInteractiveEnd = newPos; + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = oldPos; + mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = newPos; } else - mInteractiveStart = mInteractiveEnd = newPos; - SetSelection(mInteractiveStart, mInteractiveEnd); + mState.mCursors[mState.mCurrentCursor].mInteractiveStart = mState.mCursors[mState.mCurrentCursor].mInteractiveEnd = newPos; + SetSelection(mState.mCursors[mState.mCurrentCursor].mInteractiveStart, mState.mCursors[mState.mCurrentCursor].mInteractiveEnd); } void TextEditor::MoveHome(bool aSelect) { - auto oldPos = mState.mCursorPosition; - SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); - - if (mState.mCursorPosition != oldPos) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (aSelect) + auto oldPos = mState.mCursors[c].mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursors[c].mCursorPosition.mLine, 0), c); + + if (mState.mCursors[c].mCursorPosition != oldPos) { - if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; - else if (oldPos == mInteractiveEnd) - mInteractiveEnd = mState.mCursorPosition; - else + if (aSelect) { - mInteractiveStart = mState.mCursorPosition; - mInteractiveEnd = oldPos; + if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + mState.mCursors[c].mInteractiveEnd = oldPos; + } } + else + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, SelectionMode::Normal, c); } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd); } } void TextEditor::MoveEnd(bool aSelect) { - auto oldPos = mState.mCursorPosition; - SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); - - if (mState.mCursorPosition != oldPos) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (aSelect) + auto oldPos = mState.mCursors[c].mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursors[c].mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine)), c); + + if (mState.mCursors[c].mCursorPosition != oldPos) { - if (oldPos == mInteractiveEnd) - mInteractiveEnd = mState.mCursorPosition; - else if (oldPos == mInteractiveStart) - mInteractiveStart = mState.mCursorPosition; - else + if (aSelect) { - mInteractiveStart = oldPos; - mInteractiveEnd = mState.mCursorPosition; + if (oldPos == mState.mCursors[c].mInteractiveEnd) + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + else if (oldPos == mState.mCursors[c].mInteractiveStart) + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mCursorPosition; + else + { + mState.mCursors[c].mInteractiveStart = oldPos; + mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + } } + else + mState.mCursors[c].mInteractiveStart = mState.mCursors[c].mInteractiveEnd = mState.mCursors[c].mCursorPosition; + SetSelection(mState.mCursors[c].mInteractiveStart, mState.mCursors[c].mInteractiveEnd, SelectionMode::Normal, c); } - else - mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; - SetSelection(mInteractiveStart, mInteractiveEnd); } } -void TextEditor::Delete() +void TextEditor::Delete(bool aWordMode) { assert(!mReadOnly); @@ -1808,53 +2081,83 @@ void TextEditor::Delete() if (HasSelection()) { - u.mRemoved = GetSelectedText(); - u.mRemovedStart = mState.mSelectionStart; - u.mRemovedEnd = mState.mSelectionEnd; - - DeleteSelection(); + for (int c = mState.mCurrentCursor; c > -1; c--) + { + u.mRemoved.push_back({ GetSelectedText(c), mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd }); + DeleteSelection(c); + } } else { - auto pos = GetActualCursorCoordinates(); - SetCursorPosition(pos); - auto& line = mLines[pos.mLine]; - - if (pos.mColumn == GetLineMaxColumn(pos.mLine)) + std::vector positions; + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (pos.mLine == (int)mLines.size() - 1) - return; + auto pos = GetActualCursorCoordinates(c); + positions.push_back(pos); + SetCursorPosition(pos, c); + auto& line = mLines[pos.mLine]; - u.mRemoved = '\n'; - u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); - Advance(u.mRemovedEnd); - - auto& nextLine = mLines[pos.mLine + 1]; - line.insert(line.end(), nextLine.begin(), nextLine.end()); - RemoveLine(pos.mLine + 1); - } - else - { - auto cindex = GetCharacterIndex(pos); - u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); - u.mRemovedEnd.mColumn++; - u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); - - auto d = UTF8CharLength(line[cindex].mChar); - while (d-- > 0 && cindex < (int)line.size()) - line.erase(line.begin() + cindex); + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) + { + if (pos.mLine == (int)mLines.size() - 1) + return; + + Coordinates startCoords = GetActualCursorCoordinates(c); + Coordinates endCoords = startCoords; + Advance(endCoords); + u.mRemoved.push_back({ "\n", startCoords , endCoords }); + + auto& nextLine = mLines[pos.mLine + 1]; + AddGlyphsToLine(pos.mLine, line.size(), nextLine.begin(), nextLine.end()); + for (int otherCursor = c + 1; + otherCursor <= mState.mCurrentCursor && mState.mCursors[otherCursor].mCursorPosition.mLine == pos.mLine + 1; + otherCursor++) // move up cursors in next line + { + int otherCursorCharIndex = GetCharacterIndex(mState.mCursors[otherCursor].mCursorPosition); + int otherCursorNewCharIndex = GetCharacterIndex(pos) + otherCursorCharIndex; + auto targetCoords = Coordinates(pos.mLine, GetCharacterColumn(pos.mLine, otherCursorNewCharIndex)); + SetCursorPosition(targetCoords, otherCursor); + } + RemoveLine(pos.mLine + 1); + } + else + { + if (aWordMode) + { + Coordinates end = FindWordEnd(mState.mCursors[c].mCursorPosition); + u.mRemoved.push_back({ GetText(mState.mCursors[c].mCursorPosition, end), mState.mCursors[c].mCursorPosition , end }); + DeleteRange(mState.mCursors[c].mCursorPosition, end); + int charactersDeleted = end.mColumn - mState.mCursors[c].mCursorPosition.mColumn; + } + else + { + auto cindex = GetCharacterIndex(pos); + + Coordinates start = GetActualCursorCoordinates(c); + Coordinates end = start; + end.mColumn++; + if (GetText(start, end).compare("\n") == 0) + std::cout << "asdf\n"; + u.mRemoved.push_back({ GetText(start, end), start, end }); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + RemoveGlyphsFromLine(pos.mLine, cindex, cindex + 1); + } + } } mTextChanged = true; - Colorize(pos.mLine, 1); + for (const auto& pos : positions) + Colorize(pos.mLine, 1); } u.mAfter = mState; AddUndo(u); } -void TextEditor::Backspace() +void TextEditor::Backspace(bool aWordMode) { assert(!mReadOnly); @@ -1866,70 +2169,108 @@ void TextEditor::Backspace() if (HasSelection()) { - u.mRemoved = GetSelectedText(); - u.mRemovedStart = mState.mSelectionStart; - u.mRemovedEnd = mState.mSelectionEnd; - - DeleteSelection(); + for (int c = mState.mCurrentCursor; c > -1; c--) + { + u.mRemoved.push_back({ GetSelectedText(c), mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd }); + DeleteSelection(c); + } } else { - auto pos = GetActualCursorCoordinates(); - SetCursorPosition(pos); - - if (mState.mCursorPosition.mColumn == 0) + for (int c = 0; c <= mState.mCurrentCursor; c++) { - if (mState.mCursorPosition.mLine == 0) - return; + auto pos = GetActualCursorCoordinates(c); + SetCursorPosition(pos, c); - u.mRemoved = '\n'; - u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); - Advance(u.mRemovedEnd); + if (mState.mCursors[c].mCursorPosition.mColumn == 0) + { + if (mState.mCursors[c].mCursorPosition.mLine == 0) + return; + + Coordinates startCoords = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Coordinates endCoords = startCoords; + Advance(endCoords); + u.mRemoved.push_back({ "\n", startCoords, endCoords }); + + auto& line = mLines[mState.mCursors[c].mCursorPosition.mLine]; + int prevLineIndex = mState.mCursors[c].mCursorPosition.mLine - 1; + auto& prevLine = mLines[prevLineIndex]; + auto prevSize = GetLineMaxColumn(prevLineIndex); + AddGlyphsToLine(prevLineIndex, prevLine.size(), line.begin(), line.end()); + for (int otherCursor = c + 1; + otherCursor <= mState.mCurrentCursor && mState.mCursors[otherCursor].mCursorPosition.mLine == mState.mCursors[c].mCursorPosition.mLine; + otherCursor++) // move up cursors in same line + { + int otherCursorCharIndex = GetCharacterIndex(mState.mCursors[otherCursor].mCursorPosition); + int otherCursorNewCharIndex = GetCharacterIndex({ prevLineIndex, prevSize }) + otherCursorCharIndex; + auto targetCoords = Coordinates(prevLineIndex, GetCharacterColumn(prevLineIndex, otherCursorNewCharIndex)); + SetCursorPosition(targetCoords, otherCursor); + } - auto& line = mLines[mState.mCursorPosition.mLine]; - auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; - auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); - prevLine.insert(prevLine.end(), line.begin(), line.end()); + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursors[c].mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); - ErrorMarkers etmp; - for (auto& i : mErrorMarkers) - etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); - mErrorMarkers = std::move(etmp); + RemoveLine(mState.mCursors[c].mCursorPosition.mLine); + --mState.mCursors[c].mCursorPosition.mLine; + mState.mCursors[c].mCursorPosition.mColumn = prevSize; + mState.mCursors[c].mCursorPositionChanged = true; + } + else + { + auto& line = mLines[mState.mCursors[c].mCursorPosition.mLine]; - RemoveLine(mState.mCursorPosition.mLine); - --mState.mCursorPosition.mLine; - mState.mCursorPosition.mColumn = prevSize; - } - else - { - auto& line = mLines[mState.mCursorPosition.mLine]; - auto cindex = GetCharacterIndex(pos) - 1; - auto cend = cindex + 1; - while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) - --cindex; + if (aWordMode) + { + Coordinates start = FindWordStart(mState.mCursors[c].mCursorPosition - Coordinates(0, 1)); + u.mRemoved.push_back({ GetText(start, mState.mCursors[c].mCursorPosition) , start, mState.mCursors[c].mCursorPosition }); + DeleteRange(start, mState.mCursors[c].mCursorPosition); + int charactersDeleted = mState.mCursors[c].mCursorPosition.mColumn - start.mColumn; + mState.mCursors[c].mCursorPosition.mColumn -= charactersDeleted; + } + else + { + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) + --cindex; - //if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) - // --cindex; + //if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; - u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); - --u.mRemovedStart.mColumn; + Selection removed; + removed.mStart = removed.mEnd = GetActualCursorCoordinates(c); - if (line[cindex].mChar == '\t') - mState.mCursorPosition.mColumn -= mTabSize; - else - --mState.mCursorPosition.mColumn; + if (line[cindex].mChar == '\t') + { + int tabStartColumn = GetCharacterColumn(removed.mStart.mLine, cindex); + int tabLength = removed.mStart.mColumn - tabStartColumn; + mState.mCursors[c].mCursorPosition.mColumn -= tabLength; + removed.mStart.mColumn -= tabLength; + } + else + { + --mState.mCursors[c].mCursorPosition.mColumn; + --removed.mStart.mColumn; + } - while (cindex < line.size() && cend-- > cindex) - { - u.mRemoved += line[cindex].mChar; - line.erase(line.begin() + cindex); + while (cindex < line.size() && cend-- > cindex) + { + removed.mText += line[cindex].mChar; + RemoveGlyphsFromLine(mState.mCursors[c].mCursorPosition.mLine, cindex, cindex + 1); + } + u.mRemoved.push_back(removed); + } + mState.mCursors[c].mCursorPositionChanged = true; } } mTextChanged = true; EnsureCursorVisible(); - Colorize(mState.mCursorPosition.mLine, 1); + for (int c = 0; c <= mState.mCurrentCursor; c++) + Colorize(mState.mCursors[c].mCursorPosition.mLine, 1); } u.mAfter = mState; @@ -1949,14 +2290,18 @@ void TextEditor::SelectAll() bool TextEditor::HasSelection() const { - return mState.mSelectionEnd > mState.mSelectionStart; + for (int c = 0; c <= mState.mCurrentCursor; c++) + if (mState.mCursors[c].mSelectionEnd > mState.mCursors[c].mSelectionStart) + return true; + return false; } void TextEditor::Copy() { if (HasSelection()) { - ImGui::SetClipboardText(GetSelectedText().c_str()); + std::string clipboardText = GetClipboardText(); + ImGui::SetClipboardText(clipboardText.c_str()); } else { @@ -1983,12 +2328,13 @@ void TextEditor::Cut() { UndoRecord u; u.mBefore = mState; - u.mRemoved = GetSelectedText(); - u.mRemovedStart = mState.mSelectionStart; - u.mRemovedEnd = mState.mSelectionEnd; Copy(); - DeleteSelection(); + for (int c = mState.mCurrentCursor; c > -1; c--) + { + u.mRemoved.push_back({ GetSelectedText(c), mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd }); + DeleteSelection(c); + } u.mAfter = mState; AddUndo(u); @@ -2001,26 +2347,55 @@ void TextEditor::Paste() if (IsReadOnly()) return; - auto clipText = ImGui::GetClipboardText(); - if (clipText != nullptr && strlen(clipText) > 0) + // check if we should do multicursor paste + std::string clipText = ImGui::GetClipboardText(); + bool canPasteToMultipleCursors = false; + std::vector> clipTextLines; + if (mState.mCurrentCursor > 0) + { + clipTextLines.push_back({ 0,0 }); + for (int i = 0; i < clipText.length(); i++) + { + if (clipText[i] == '\n') + { + clipTextLines.back().second = i; + clipTextLines.push_back({ i + 1, 0 }); + } + } + clipTextLines.back().second = clipText.length(); + canPasteToMultipleCursors = clipTextLines.size() == mState.mCurrentCursor + 1; + } + + if (clipText.length() > 0) { UndoRecord u; u.mBefore = mState; if (HasSelection()) { - u.mRemoved = GetSelectedText(); - u.mRemovedStart = mState.mSelectionStart; - u.mRemovedEnd = mState.mSelectionEnd; - DeleteSelection(); + for (int c = mState.mCurrentCursor; c > -1; c--) + { + u.mRemoved.push_back({ GetSelectedText(c), mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd }); + DeleteSelection(c); + } } - u.mAdded = clipText; - u.mAddedStart = GetActualCursorCoordinates(); - - InsertText(clipText); + for (int c = mState.mCurrentCursor; c > -1; c--) + { + Coordinates start = GetActualCursorCoordinates(c); + if (canPasteToMultipleCursors) + { + std::string clipSubText = clipText.substr(clipTextLines[c].first, clipTextLines[c].second - clipTextLines[c].first); + InsertText(clipSubText, c); + u.mAdded.push_back({ clipSubText, start, GetActualCursorCoordinates(c) }); + } + else + { + InsertText(clipText, c); + u.mAdded.push_back({ clipText, start, GetActualCursorCoordinates(c) }); + } + } - u.mAddedEnd = GetActualCursorCoordinates(); u.mAfter = mState; AddUndo(u); } @@ -2051,27 +2426,57 @@ void TextEditor::Redo(int aSteps) const TextEditor::Palette & TextEditor::GetDarkPalette() { const static Palette p = { { - 0xff7f7f7f, // Default - 0xffd69c56, // Keyword - 0xff00ff00, // Number - 0xff7070e0, // String - 0xff70a0e0, // Char literal + 0xb0b0b0ff, // Default + 0x569cd6ff, // Keyword + 0x00ff00ff, // Number + 0xe07070ff, // String + 0xe0a070ff, // Char literal 0xffffffff, // Punctuation - 0xff408080, // Preprocessor - 0xffaaaaaa, // Identifier - 0xff9bc64d, // Known identifier - 0xffc040a0, // Preproc identifier - 0xff206020, // Comment (single line) - 0xff406020, // Comment (multi line) - 0xff101010, // Background - 0xffe0e0e0, // Cursor - 0x80a06020, // Selection - 0x800020ff, // ErrorMarker - 0x40f08000, // Breakpoint - 0xff707000, // Line number - 0x40000000, // Current line fill - 0x40808080, // Current line fill (inactive) - 0x40a0a0a0, // Current line edge + 0x808040ff, // Preprocessor + 0xaaaaaaff, // Identifier + 0x4dc69bff, // Known identifier + 0xa040c0ff, // Preproc identifier + 0x206020ff, // Comment (single line) + 0x206040ff, // Comment (multi line) + 0x101010ff, // Background + 0xe0e0e0ff, // Cursor + 0x2060a080, // Selection + 0xff200080, // ErrorMarker + 0x90909090, // ControlCharacter + 0x0080f040, // Breakpoint + 0x007070ff, // Line number + 0x00000040, // Current line fill + 0x80808040, // Current line fill (inactive) + 0xa0a0a040, // Current line edge + } }; + return p; +} + +const TextEditor::Palette& TextEditor::GetMarianaPalette() +{ + const static Palette p = { { + 0xffffffff, // Default + 0xc695c6ff, // Keyword + 0xf9ae58ff, // Number + 0x99c794ff, // String + 0xe0a070ff, // Char literal + 0x5fb4b4ff, // Punctuation + 0x808040ff, // Preprocessor + 0xffffffff, // Identifier + 0x4dc69bff, // Known identifier + 0xe0a0ffff, // Preproc identifier + 0xa6acb9ff, // Comment (single line) + 0xa6acb9ff, // Comment (multi line) + 0x303841ff, // Background + 0xe0e0e0ff, // Cursor + 0x4e5a6580, // Selection + 0xec5f6680, // ErrorMarker + 0xffffff30, // ControlCharacter + 0x0080f040, // Breakpoint + 0xffffffb0, // Line number + 0x4e5a6580, // Current line fill + 0x4e5a6530, // Current line fill (inactive) + 0x4e5a65b0, // Current line edge } }; return p; } @@ -2079,27 +2484,28 @@ const TextEditor::Palette & TextEditor::GetDarkPalette() const TextEditor::Palette & TextEditor::GetLightPalette() { const static Palette p = { { - 0xff7f7f7f, // None - 0xffff0c06, // Keyword - 0xff008000, // Number - 0xff2020a0, // String - 0xff304070, // Char literal - 0xff000000, // Punctuation - 0xff406060, // Preprocessor - 0xff404040, // Identifier - 0xff606010, // Known identifier - 0xffc040a0, // Preproc identifier - 0xff205020, // Comment (single line) - 0xff405020, // Comment (multi line) + 0x404040ff, // None + 0x060cffff, // Keyword + 0x008000ff, // Number + 0xa02020ff, // String + 0x704030ff, // Char literal + 0x000000ff, // Punctuation + 0x606040ff, // Preprocessor + 0x404040ff, // Identifier + 0x106060ff, // Known identifier + 0xa040c0ff, // Preproc identifier + 0x205020ff, // Comment (single line) + 0x205040ff, // Comment (multi line) 0xffffffff, // Background - 0xff000000, // Cursor - 0x80600000, // Selection - 0xa00010ff, // ErrorMarker - 0x80f08000, // Breakpoint - 0xff505000, // Line number - 0x40000000, // Current line fill - 0x40808080, // Current line fill (inactive) - 0x40000000, // Current line edge + 0x000000ff, // Cursor + 0x00006040, // Selection + 0xff1000a0, // ErrorMarker + 0x90909090, // ControlCharacter + 0x0080f080, // Breakpoint + 0x005050ff, // Line number + 0x00000040, // Current line fill + 0x80808040, // Current line fill (inactive) + 0x00000040, // Current line edge } }; return p; } @@ -2107,31 +2513,77 @@ const TextEditor::Palette & TextEditor::GetLightPalette() const TextEditor::Palette & TextEditor::GetRetroBluePalette() { const static Palette p = { { - 0xff00ffff, // None - 0xffffff00, // Keyword - 0xff00ff00, // Number - 0xff808000, // String - 0xff808000, // Char literal + 0xffff00ff, // None + 0x00ffffff, // Keyword + 0x00ff00ff, // Number + 0x008080ff, // String + 0x008080ff, // Char literal 0xffffffff, // Punctuation - 0xff008000, // Preprocessor - 0xff00ffff, // Identifier + 0x008000ff, // Preprocessor + 0xffff00ff, // Identifier 0xffffffff, // Known identifier - 0xffff00ff, // Preproc identifier - 0xff808080, // Comment (single line) - 0xff404040, // Comment (multi line) - 0xff800000, // Background - 0xff0080ff, // Cursor - 0x80ffff00, // Selection - 0xa00000ff, // ErrorMarker - 0x80ff8000, // Breakpoint - 0xff808000, // Line number - 0x40000000, // Current line fill - 0x40808080, // Current line fill (inactive) - 0x40000000, // Current line edge + 0xff00ffff, // Preproc identifier + 0x808080ff, // Comment (single line) + 0x404040ff, // Comment (multi line) + 0x000080ff, // Background + 0xff8000ff, // Cursor + 0x00ffff80, // Selection + 0xff0000a0, // ErrorMarker + 0x0080ff80, // Breakpoint + 0x008080ff, // Line number + 0x00000040, // Current line fill + 0x80808040, // Current line fill (inactive) + 0x00000040, // Current line edge } }; return p; } +void TextEditor::MergeCursorsIfPossible() +{ + // requires the cursors to be sorted from top to bottom + std::unordered_set cursorsToDelete; + if (HasSelection()) + { + // merge cursors if they overlap + for (int c = mState.mCurrentCursor; c > 0; c--)// iterate backwards through pairs + { + int pc = c - 1; + + bool pcContainsC = mState.mCursors[pc].mSelectionEnd >= mState.mCursors[c].mSelectionEnd; + bool pcContainsStartOfC = mState.mCursors[pc].mSelectionEnd >= mState.mCursors[c].mSelectionStart; + + if (pcContainsC) + { + cursorsToDelete.insert(c); + } + else if (pcContainsStartOfC) + { + mState.mCursors[pc].mSelectionEnd = mState.mCursors[c].mSelectionEnd; + mState.mCursors[pc].mInteractiveEnd = mState.mCursors[c].mSelectionEnd; + mState.mCursors[pc].mInteractiveStart = mState.mCursors[pc].mSelectionStart; + mState.mCursors[pc].mCursorPosition = mState.mCursors[c].mSelectionEnd; + cursorsToDelete.insert(c); + } + } + } + else + { + // merge cursors if they are at the same position + for (int c = mState.mCurrentCursor; c > 0; c--)// iterate backwards through pairs + { + int pc = c - 1; + if (mState.mCursors[pc].mCursorPosition == mState.mCursors[c].mCursorPosition) + cursorsToDelete.insert(c); + } + } + for (int c = mState.mCurrentCursor; c > -1; c--)// iterate backwards through each of them + { + if (cursorsToDelete.find(c) != cursorsToDelete.end()) + mState.mCursors.erase(mState.mCursors.begin() + c); + } + mState.mCurrentCursor -= cursorsToDelete.size(); +} + std::string TextEditor::GetText() const { @@ -2161,17 +2613,35 @@ std::vector TextEditor::GetTextLines() const return result; } -std::string TextEditor::GetSelectedText() const +std::string TextEditor::GetClipboardText() const { - return GetText(mState.mSelectionStart, mState.mSelectionEnd); + std::string result; + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mSelectionStart < mState.mCursors[c].mSelectionEnd) + { + if (result.length() != 0) + result += '\n'; + result += GetText(mState.mCursors[c].mSelectionStart, mState.mCursors[c].mSelectionEnd); + } + } + return result; +} + +std::string TextEditor::GetSelectedText(int aCursor) const +{ + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + + return GetText(mState.mCursors[aCursor].mSelectionStart, mState.mCursors[aCursor].mSelectionEnd); } std::string TextEditor::GetCurrentLineText()const { - auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + auto lineLength = GetLineMaxColumn(mState.mCursors[mState.mCurrentCursor].mCursorPosition.mLine); return GetText( - Coordinates(mState.mCursorPosition.mLine, 0), - Coordinates(mState.mCursorPosition.mLine, lineLength)); + Coordinates(mState.mCursors[mState.mCurrentCursor].mCursorPosition.mLine, 0), + Coordinates(mState.mCursors[mState.mCurrentCursor].mCursorPosition.mLine, lineLength)); } void TextEditor::ProcessInputs() @@ -2389,7 +2859,7 @@ void TextEditor::ColorizeInternal() withinSingleLineComment = true; } - inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); line[currentIndex].mMultiLineComment = inComment; line[currentIndex].mComment = withinSingleLineComment; @@ -2465,8 +2935,11 @@ float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const return distance; } -void TextEditor::EnsureCursorVisible() +void TextEditor::EnsureCursorVisible(int aCursor) { + if (aCursor == -1) + aCursor = mState.mCurrentCursor; + if (!mWithinRender) { mScrollToCursor = true; @@ -2485,7 +2958,7 @@ void TextEditor::EnsureCursorVisible() auto left = (int)ceil(scrollX / mCharAdvance.x); auto right = (int)ceil((scrollX + width) / mCharAdvance.x); - auto pos = GetActualCursorCoordinates(); + auto pos = GetActualCursorCoordinates(aCursor); auto len = TextDistanceToLineStart(pos); if (pos.mLine < top) @@ -2505,40 +2978,42 @@ int TextEditor::GetPageSize() const } TextEditor::UndoRecord::UndoRecord( - const std::string& aAdded, - const TextEditor::Coordinates aAddedStart, - const TextEditor::Coordinates aAddedEnd, - const std::string& aRemoved, - const TextEditor::Coordinates aRemovedStart, - const TextEditor::Coordinates aRemovedEnd, + const std::vector& aAdded, + const std::vector& aRemoved, TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter) : mAdded(aAdded) - , mAddedStart(aAddedStart) - , mAddedEnd(aAddedEnd) , mRemoved(aRemoved) - , mRemovedStart(aRemovedStart) - , mRemovedEnd(aRemovedEnd) , mBefore(aBefore) , mAfter(aAfter) { - assert(mAddedStart <= mAddedEnd); - assert(mRemovedStart <= mRemovedEnd); + for (const Selection& added : mAdded) + assert(added.mStart <= added.mEnd); + for (const Selection& removed : mRemoved) + assert(removed.mStart <= removed.mEnd); } void TextEditor::UndoRecord::Undo(TextEditor * aEditor) { - if (!mAdded.empty()) + for (int i = mAdded.size() - 1; i > -1; i--) { - aEditor->DeleteRange(mAddedStart, mAddedEnd); - aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); + const Selection& added = mAdded[i]; + if (!added.mText.empty()) + { + aEditor->DeleteRange(added.mStart, added.mEnd); + aEditor->Colorize(added.mStart.mLine - 1, added.mEnd.mLine - added.mStart.mLine + 2); + } } - if (!mRemoved.empty()) + for (int i = mRemoved.size() - 1; i > -1; i--) { - auto start = mRemovedStart; - aEditor->InsertTextAt(start, mRemoved.c_str()); - aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); + const Selection& removed = mRemoved[i]; + if (!removed.mText.empty()) + { + auto start = removed.mStart; + aEditor->InsertTextAt(start, removed.mText.c_str()); + aEditor->Colorize(removed.mStart.mLine - 1, removed.mEnd.mLine - removed.mStart.mLine + 2); + } } aEditor->mState = mBefore; @@ -2548,17 +3023,25 @@ void TextEditor::UndoRecord::Undo(TextEditor * aEditor) void TextEditor::UndoRecord::Redo(TextEditor * aEditor) { - if (!mRemoved.empty()) + for (int i = 0; i < mRemoved.size(); i++) { - aEditor->DeleteRange(mRemovedStart, mRemovedEnd); - aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + const Selection& removed = mRemoved[i]; + if (!removed.mText.empty()) + { + aEditor->DeleteRange(removed.mStart, removed.mEnd); + aEditor->Colorize(removed.mStart.mLine - 1, removed.mEnd.mLine - removed.mStart.mLine + 1); + } } - if (!mAdded.empty()) + for (int i = 0; i < mAdded.size(); i++) { - auto start = mAddedStart; - aEditor->InsertTextAt(start, mAdded.c_str()); - aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + const Selection& added = mAdded[i]; + if (!added.mText.empty()) + { + auto start = added.mStart; + aEditor->InsertTextAt(start, added.mText.c_str()); + aEditor->Colorize(added.mStart.mLine - 1, added.mEnd.mLine - added.mStart.mLine + 1); + } } aEditor->mState = mAfter; diff --git a/TextEditor.h b/TextEditor.h index f4d5dc2c..90d7a8cf 100644 --- a/TextEditor.h +++ b/TextEditor.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -10,7 +11,7 @@ #include #include "imgui.h" -class TextEditor +class IMGUI_API TextEditor { public: enum class PaletteIndex @@ -31,6 +32,7 @@ class TextEditor Cursor, Selection, ErrorMarker, + ControlCharacter, Breakpoint, LineNumber, CurrentLineFill, @@ -117,6 +119,11 @@ class TextEditor return mLine > o.mLine; return mColumn >= o.mColumn; } + + Coordinates operator -(const Coordinates& o) + { + return Coordinates(mLine - o.mLine, mColumn - o.mColumn); + } }; struct Identifier @@ -182,6 +189,13 @@ class TextEditor static const LanguageDefinition& Lua(); }; + struct Selection + { + std::string mText; + TextEditor::Coordinates mStart; + TextEditor::Coordinates mEnd; + }; + TextEditor(); ~TextEditor(); @@ -201,7 +215,8 @@ class TextEditor void SetTextLines(const std::vector& aLines); std::vector GetTextLines() const; - std::string GetSelectedText() const; + std::string GetClipboardText() const; + std::string GetSelectedText(int aCursor = -1) const; std::string GetCurrentLineText()const; int GetTotalLines() const { return (int)mLines.size(); } @@ -210,16 +225,42 @@ class TextEditor void SetReadOnly(bool aValue); bool IsReadOnly() const { return mReadOnly; } bool IsTextChanged() const { return mTextChanged; } - bool IsCursorPositionChanged() const { return mCursorPositionChanged; } + + void OnCursorPositionChanged(int aCursor); bool IsColorizerEnabled() const { return mColorizerEnabled; } void SetColorizerEnable(bool aValue); Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); } - void SetCursorPosition(const Coordinates& aPosition); + void SetCursorPosition(const Coordinates& aPosition, int aCursor = -1); + + inline void OnLineDeleted(int lineNumber) + { + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mCursorPosition.mLine > lineNumber) + mState.mCursors[c].mCursorPosition.mLine--; + } + } + inline void OnLinesDeleted(int aFirstLineIndex, int aLastLineIndex) + { + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mCursorPosition.mLine >= aFirstLineIndex) + mState.mCursors[c].mCursorPosition.mLine -= aLastLineIndex - aFirstLineIndex; + } + } + inline void OnLineAdded(int aLineIndex) + { + for (int c = 0; c <= mState.mCurrentCursor; c++) + { + if (mState.mCursors[c].mCursorPosition.mLine > aLineIndex) + mState.mCursors[c].mCursorPosition.mLine++; + } + } inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;} - inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; } + inline bool IsHandleMouseInputsEnabled() const { return mHandleMouseInputs; } inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;} inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; } @@ -233,11 +274,20 @@ class TextEditor inline void SetShowShortTabGlyphs(bool aValue) { mShowShortTabGlyphs = aValue; } inline bool IsShowingShortTabGlyphs() const { return mShowShortTabGlyphs; } + inline ImVec4 U32ColorToVec4(ImU32 in) { + float s = 1.0f / 255.0f; + return ImVec4( + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s); + } + void SetTabSize(int aValue); inline int GetTabSize() const { return mTabSize; } - void InsertText(const std::string& aValue); - void InsertText(const char* aValue); + void InsertText(const std::string& aValue, int aCursor = -1); + void InsertText(const char* aValue, int aCursor = -1); void MoveUp(int aAmount = 1, bool aSelect = false); void MoveDown(int aAmount = 1, bool aSelect = false); @@ -248,9 +298,9 @@ class TextEditor void MoveHome(bool aSelect = false); void MoveEnd(bool aSelect = false); - void SetSelectionStart(const Coordinates& aPosition); - void SetSelectionEnd(const Coordinates& aPosition); - void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal); + void SetSelectionStart(const Coordinates& aPosition, int aCursor = -1); + void SetSelectionEnd(const Coordinates& aPosition, int aCursor = -1); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal, int aCursor = -1, bool isSpawningNewCursor = false); void SelectWordUnderCursor(); void SelectAll(); bool HasSelection() const; @@ -258,27 +308,45 @@ class TextEditor void Copy(); void Cut(); void Paste(); - void Delete(); + void Delete(bool aWordMode = false); bool CanUndo() const; bool CanRedo() const; void Undo(int aSteps = 1); void Redo(int aSteps = 1); + static const Palette& GetMarianaPalette(); static const Palette& GetDarkPalette(); static const Palette& GetLightPalette(); static const Palette& GetRetroBluePalette(); -private: +public: typedef std::vector> RegexList; + struct Cursor + { + Coordinates mCursorPosition = { 0, 0 }; + Coordinates mSelectionStart = { 0,0 }; + Coordinates mSelectionEnd = { 0,0 }; + Coordinates mInteractiveStart = { 0,0 }; + Coordinates mInteractiveEnd = { 0,0 }; + bool mCursorPositionChanged = false; + }; + + //typedef std::vector EditorState; struct EditorState { - Coordinates mSelectionStart; - Coordinates mSelectionEnd; - Coordinates mCursorPosition; + int mCurrentCursor = 0; + std::vector mCursors = { {{0,0}, {0,0}, {0,0}, {0,0}} }; + void AddCursor() + { + mCurrentCursor++; + mCursors.resize(mCurrentCursor + 1); + } }; + void MergeCursorsIfPossible(); + class UndoRecord { public: @@ -286,27 +354,16 @@ class TextEditor ~UndoRecord() {} UndoRecord( - const std::string& aAdded, - const TextEditor::Coordinates aAddedStart, - const TextEditor::Coordinates aAddedEnd, - - const std::string& aRemoved, - const TextEditor::Coordinates aRemovedStart, - const TextEditor::Coordinates aRemovedEnd, - + const std::vector& aAdded, + const std::vector& aRemoved, TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter); void Undo(TextEditor* aEditor); void Redo(TextEditor* aEditor); - std::string mAdded; - Coordinates mAddedStart; - Coordinates mAddedEnd; - - std::string mRemoved; - Coordinates mRemovedStart; - Coordinates mRemovedEnd; + std::vector mAdded; + std::vector mRemoved; EditorState mBefore; EditorState mAfter; @@ -319,36 +376,43 @@ class TextEditor void ColorizeRange(int aFromLine = 0, int aToLine = 0); void ColorizeInternal(); float TextDistanceToLineStart(const Coordinates& aFrom) const; - void EnsureCursorVisible(); + void EnsureCursorVisible(int aCursor = -1); int GetPageSize() const; std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; - Coordinates GetActualCursorCoordinates() const; + Coordinates GetActualCursorCoordinates(int aCursor = -1) const; Coordinates SanitizeCoordinates(const Coordinates& aValue) const; void Advance(Coordinates& aCoordinates) const; void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); int InsertTextAt(Coordinates& aWhere, const char* aValue); void AddUndo(UndoRecord& aValue); - Coordinates ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode = false) const; + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode = false, bool* isOverLineNumber = nullptr) const; Coordinates FindWordStart(const Coordinates& aFrom) const; Coordinates FindWordEnd(const Coordinates& aFrom) const; Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndexLeftSide(const Coordinates& aCoordinates) const; int GetCharacterIndex(const Coordinates& aCoordinates) const; int GetCharacterColumn(int aLine, int aIndex) const; int GetLineCharacterCount(int aLine) const; int GetLineMaxColumn(int aLine) const; bool IsOnWordBoundary(const Coordinates& aAt) const; - void RemoveLine(int aStart, int aEnd); + void RemoveLines(int aStart, int aEnd); void RemoveLine(int aIndex); + void RemoveCurrentLines(); + void OnLineChanged(bool aBeforeChange, int aLine, int aColumn, int aCharCount, bool aDeleted); + void RemoveGlyphsFromLine(int aLine, int aStartChar, int aEndChar = -1); + void AddGlyphsToLine(int aLine, int aTargetIndex, Line::iterator aSourceStart, Line::iterator aSourceEnd); + void AddGlyphToLine(int aLine, int aTargetIndex, Glyph aGlyph); Line& InsertLine(int aIndex); void EnterCharacter(ImWchar aChar, bool aShift); - void Backspace(); - void DeleteSelection(); + void Backspace(bool aWordMode = false); + void DeleteSelection(int aCursor = -1); std::string GetWordUnderCursor() const; std::string GetWordAt(const Coordinates& aCoords) const; ImU32 GetGlyphColor(const Glyph& aGlyph) const; void HandleKeyboardInputs(); void HandleMouseInputs(); + void UpdatePalette(); void Render(); float mLineSpacing; @@ -367,7 +431,6 @@ class TextEditor bool mColorizerEnabled; float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor. int mLeftMargin; - bool mCursorPositionChanged; int mColorRangeMin, mColorRangeMax; SelectionMode mSelectionMode; bool mHandleKeyboardInputs; @@ -375,6 +438,7 @@ class TextEditor bool mIgnoreImGuiChild; bool mShowWhitespaces; bool mShowShortTabGlyphs; + bool mDraggingSelection = false; Palette mPaletteBase; Palette mPalette; @@ -385,7 +449,6 @@ class TextEditor Breakpoints mBreakpoints; ErrorMarkers mErrorMarkers; ImVec2 mCharAdvance; - Coordinates mInteractiveStart, mInteractiveEnd; std::string mLineBuffer; uint64_t mStartTime;