diff --git a/imguicolortextedit/TextEditor.cpp b/imguicolortextedit/TextEditor.cpp index caef161d..e1391d33 100644 --- a/imguicolortextedit/TextEditor.cpp +++ b/imguicolortextedit/TextEditor.cpp @@ -14,8 +14,6 @@ namespace tracy // TODO // - multiline comments vs single-line: latter is blocking start of a ML -// - handle unicode/utf -// - testing template bool equals(InputIt1 first1, InputIt1 last1, @@ -39,13 +37,20 @@ TextEditor::TextEditor() , mScrollToCursor(false) , mScrollToTop(false) , mTextChanged(false) + , 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) + , mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) { SetPalette(GetDarkPalette()); SetLanguageDefinition(LanguageDefinition::HLSL()); @@ -63,6 +68,8 @@ void TextEditor::SetLanguageDefinition(const LanguageDefinition & aLanguageDef) for (auto& r : mLanguageDefinition.mTokenRegexStrings) mRegexList.push_back(std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); } void TextEditor::SetPalette(const Palette & aValue) @@ -70,40 +77,38 @@ void TextEditor::SetPalette(const Palette & aValue) mPaletteBase = aValue; } -int TextEditor::AppendBuffer(std::string& aBuffer, char chr, int aIndex) -{ - if (chr != '\t') - { - aBuffer.push_back(chr); - return aIndex + 1; - } - else - { - //auto num = mTabSize - aIndex % mTabSize; - //for (int j = num; j > 0; --j) - // aBuffer.push_back(' '); - //return aIndex + num; - return aIndex; - } -} - std::string TextEditor::GetText(const Coordinates & aStart, const Coordinates & aEnd) const { std::string result; - int prevLineNo = aStart.mLine; - for (auto it = aStart; it <= aEnd; Advance(it)) - { - if (prevLineNo != it.mLine && it.mLine < (int)mLines.size()) - result.push_back('\n'); + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; - if (it == aEnd) + for (size_t i = lstart; i < lend; i++) + s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) + { + if (lstart >= (int)mLines.size()) break; - prevLineNo = it.mLine; - const auto& line = mLines[it.mLine]; - if (!line.empty() && it.mColumn < (int)line.size()) - result.push_back(line[it.mColumn].mChar); + auto& line = mLines[lstart]; + if (istart < (int)line.size()) + { + result += line[istart].mChar; + istart++; + } + else + { + istart = 0; + ++lstart; + result += '\n'; + } } return result; @@ -118,7 +123,6 @@ TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aVal { auto line = aValue.mLine; auto column = aValue.mColumn; - if (line >= (int)mLines.size()) { if (mLines.empty()) @@ -129,15 +133,70 @@ TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aVal else { line = (int)mLines.size() - 1; - column = (int)mLines[line].size(); + column = GetLineMaxColumn(line); } + return Coordinates(line, column); } else { - column = mLines.empty() ? 0 : std::min((int)mLines[line].size(), aValue.mColumn); + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return Coordinates(line, column); } +} - return Coordinates(line, column); +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) +{ + if ((c & 0xFE) == 0xFC) + return 6; + if ((c & 0xFC) == 0xF8) + return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) + { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c) & 0x3f)); + return 4; + } + //else if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c) & 0x3f)); + return 3; + } } void TextEditor::Advance(Coordinates & aCoordinates) const @@ -145,14 +204,19 @@ void TextEditor::Advance(Coordinates & aCoordinates) const if (aCoordinates.mLine < (int)mLines.size()) { auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); - if (aCoordinates.mColumn + 1 < (int)line.size()) - ++aCoordinates.mColumn; + if (cindex + 1 < (int)line.size()) + { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int)line.size() - 1); + } else { ++aCoordinates.mLine; - aCoordinates.mColumn = 0; + cindex = 0; } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); } } @@ -161,24 +225,30 @@ void TextEditor::DeleteRange(const Coordinates & aStart, const Coordinates & aEn assert(aEnd >= aStart); assert(!mReadOnly); + //printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn); + if (aEnd == aStart) return; + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + if (aStart.mLine == aEnd.mLine) { auto& line = mLines[aStart.mLine]; - if (aEnd.mColumn >= (int)line.size()) - line.erase(line.begin() + aStart.mColumn, line.end()); + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); else - line.erase(line.begin() + aStart.mColumn, line.begin() + aEnd.mColumn); + line.erase(line.begin() + start, line.begin() + end); } else { auto& firstLine = mLines[aStart.mLine]; auto& lastLine = mLines[aEnd.mLine]; - firstLine.erase(firstLine.begin() + aStart.mColumn, firstLine.end()); - lastLine.erase(lastLine.begin(), lastLine.begin() + aEnd.mColumn); + 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()); @@ -194,24 +264,25 @@ int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValu { assert(!mReadOnly); + int cindex = GetCharacterIndex(aWhere); int totalLines = 0; - auto chr = *aValue; - while (chr != '\0') + while (*aValue != '\0') { assert(!mLines.empty()); - if (chr == '\r') + if (*aValue == '\r') { // skip + ++aValue; } - else if (chr == '\n') + else if (*aValue == '\n') { - if (aWhere.mColumn < (int)mLines[aWhere.mLine].size()) + if (cindex < (int)mLines[aWhere.mLine].size()) { auto& newLine = InsertLine(aWhere.mLine + 1); auto& line = mLines[aWhere.mLine]; - newLine.insert(newLine.begin(), line.begin() + aWhere.mColumn, line.end()); - line.erase(line.begin() + aWhere.mColumn, line.end()); + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); } else { @@ -219,15 +290,18 @@ int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValu } ++aWhere.mLine; aWhere.mColumn = 0; + cindex = 0; ++totalLines; + ++aValue; } else { auto& line = mLines[aWhere.mLine]; - line.insert(line.begin() + aWhere.mColumn, Glyph(chr, PaletteIndex::Default)); + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); ++aWhere.mColumn; } - chr = *(++aValue); mTextChanged = true; } @@ -238,8 +312,14 @@ int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValu void TextEditor::AddUndo(UndoRecord& aValue) { assert(!mReadOnly); + //printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n", + // aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn, + // aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, + // aValue.mRemoved.c_str(), aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine, aValue.mRemovedEnd.mColumn, + // aValue.mAfter.mCursorPosition.mLine, aValue.mAfter.mCursorPosition.mColumn + // ); - mUndoBuffer.resize(mUndoIndex + 1); + mUndoBuffer.resize((size_t)(mUndoIndex + 1)); mUndoBuffer.back() = aValue; ++mUndoIndex; } @@ -251,32 +331,46 @@ TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPositi int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); - /* - Compute columnCoord according to text size - */ int columnCoord = 0; - float columnWidth = 0.0f; - std::string cumulatedString = ""; - float cumulatedStringWidth[2] = { 0.0f, 0.0f }; //( [0] is the lastest, [1] is the previous. I use that trick to check where cursor is exactly (important for tabs) if (lineNo >= 0 && lineNo < (int)mLines.size()) { auto& line = mLines.at(lineNo); - // First we find the hovered column coord. - while (mTextStart + cumulatedStringWidth[0] < local.x && - (size_t)columnCoord < line.size()) - { - cumulatedStringWidth[1] = cumulatedStringWidth[0]; - cumulatedString += line[columnCoord].mChar; - cumulatedStringWidth[0] = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, cumulatedString.c_str(), nullptr, nullptr).x; - columnWidth = (cumulatedStringWidth[0] - cumulatedStringWidth[1]); - columnCoord++; - } + int columnIndex = 0; + float columnX = 0.0f; - // Then we reduce by 1 column coord if cursor is on the left side of the hovered column. - if (mTextStart + cumulatedStringWidth[0] - columnWidth / 2.0f > local.x) - columnCoord = std::max(0, columnCoord - 1); + while ((size_t)columnIndex < line.size()) + { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; + float oldX = columnX; + float newColumnX = (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } + else + { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) + buf[i++] = line[columnIndex++].mChar; + buf[i] = '\0'; + columnWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX += columnWidth; + columnCoord++; + } + } } return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); @@ -289,18 +383,31 @@ TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates & aFrom) con return at; auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); - if (at.mColumn >= (int)line.size()) + if (cindex >= (int)line.size()) return at; - auto cstart = (PaletteIndex)line[at.mColumn].mColorIndex; - while (at.mColumn > 0) + while (cindex > 0 && isspace(line[cindex].mChar)) + --cindex; + + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex > 0) { - if (cstart != (PaletteIndex)line[at.mColumn - 1].mColorIndex) - break; - --at.mColumn; + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) + { + cindex++; + break; + } + if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex) + break; + } + --cindex; } - return at; + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); } TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates & aFrom) const @@ -310,30 +417,162 @@ TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates & aFrom) const return at; auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); - if (at.mColumn >= (int)line.size()) + if (cindex >= (int)line.size()) return at; - auto cstart = (PaletteIndex)line[at.mColumn].mColorIndex; - while (at.mColumn < (int)line.size()) + bool prevspace = (bool)isspace(line[cindex].mChar); + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex < (int)line.size()) { - if (cstart != (PaletteIndex)line[at.mColumn].mColorIndex) + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex)line[cindex].mColorIndex) break; - ++at.mColumn; + + if (prevspace != !!isspace(c)) + { + if (isspace(c)) + while (cindex < (int)line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; } + return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int)mLines[at.mLine].size()) + { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar); + skip = isword; + } + + while (!isword || skip) + { + if (at.mLine >= mLines.size()) + { + auto l = std::max(0, (int) mLines.size() - 1); + return Coordinates(l, GetLineMaxColumn(l)); + } + + auto& line = mLines[at.mLine]; + if (cindex < (int)line.size()) + { + isword = isalnum(line[cindex].mChar); + + if (isword && !skip) + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); + + if (!isword) + skip = false; + + cindex++; + } + else + { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + return at; } +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine >= mLines.size()) + return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) + { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int)line.size()) + { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) + i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size(); ) + { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + bool TextEditor::IsOnWordBoundary(const Coordinates & aAt) const { if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0) return true; auto& line = mLines[aAt.mLine]; - if (aAt.mColumn >= (int)line.size()) + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int)line.size()) return true; - return line[aAt.mColumn].mColorIndex != line[aAt.mColumn - 1].mColorIndex; + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); } void TextEditor::RemoveLine(int aStart, int aEnd) @@ -429,14 +668,19 @@ std::string TextEditor::GetWordAt(const Coordinates & aCoords) const std::string r; - for (auto it = start; it < end; Advance(it)) - r.push_back(mLines[it.mLine][it.mColumn].mChar); + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(mLines[aCoords.mLine][it].mChar); return r; } ImU32 TextEditor::GetGlyphColor(const Glyph & aGlyph) const { + if (!mColorizerEnabled) + return mPalette[(int)PaletteIndex::Default]; if (aGlyph.mComment) return mPalette[(int)PaletteIndex::Comment]; if (aGlyph.mMultiLineComment) @@ -451,7 +695,6 @@ ImU32 TextEditor::GetGlyphColor(const Glyph & aGlyph) const const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); } - return color; } @@ -500,7 +743,7 @@ void TextEditor::HandleKeyboardInputs() else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) Delete(); else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) - BackSpace(); + Backspace(); else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) mOverwrite ^= true; else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) @@ -521,18 +764,14 @@ void TextEditor::HandleKeyboardInputs() EnterCharacter('\n', false); else if (!IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab))) EnterCharacter('\t', shift); - else if (!IsReadOnly() && !ctrl && !alt) + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) { for (int i = 0; i < io.InputQueueCharacters.Size; i++) { - auto c = (unsigned char)io.InputQueueCharacters[i]; - if (c != 0) - { - if (isprint(c) || isspace(c)) - { - EnterCharacter((char)c, shift); - } - } + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) + EnterCharacter(c, shift); } io.InputQueueCharacters.resize(0); } @@ -556,14 +795,14 @@ void TextEditor::HandleMouseInputs() auto tripleClick = click && !doubleClick && (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); /* - Left mouse button triple click + Left mouse button triple click */ if (tripleClick) { if (!ctrl) { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); mSelectionMode = SelectionMode::Line; SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); } @@ -572,14 +811,14 @@ void TextEditor::HandleMouseInputs() } /* - Left mouse button double click + Left mouse button double click */ else if (doubleClick) { if (!ctrl) { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); if (mSelectionMode == SelectionMode::Line) mSelectionMode = SelectionMode::Normal; else @@ -591,11 +830,11 @@ void TextEditor::HandleMouseInputs() } /* - Left mouse button click + Left mouse button click */ else if (click) { - mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); if (ctrl) mSelectionMode = SelectionMode::Word; else @@ -608,7 +847,7 @@ void TextEditor::HandleMouseInputs() else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) { io.WantCaptureMouse = true; - mState.mCursorPosition = mInteractiveEnd = SanitizeCoordinates(ScreenPosToCoordinates(ImGui::GetMousePos())); + mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); } } @@ -628,14 +867,13 @@ void TextEditor::Render() color.w *= ImGui::GetStyle().Alpha; mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); } - - static std::string buffer; - assert(buffer.empty()); - + + assert(mLineBuffer.empty()); + auto contentSize = ImGui::GetWindowContentRegionMax(); auto drawList = ImGui::GetWindowDrawList(); float longest(mTextStart); - + if (mScrollToTop) { mScrollToTop = false; @@ -657,7 +895,6 @@ void TextEditor::Render() if (!mLines.empty()) { - auto fontScale = ImGui::GetFontSize() / ImGui::GetFont()->FontSize; float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; while (lineNo <= lineMax) @@ -666,10 +903,10 @@ void TextEditor::Render() ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); auto& line = mLines[lineNo]; - longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, (int)line.size())), longest); + longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest); auto columnNo = 0; Coordinates lineStartCoord(lineNo, 0); - Coordinates lineEndCoord(lineNo, (int)line.size()); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); // Draw selection for the current line float sstart = -1.0f; @@ -723,15 +960,15 @@ void TextEditor::Render() // Draw line number (right aligned) snprintf(buf, 16, "%d ", lineNo + 1); - + 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); - // Highlight the current line (where the cursor is) if (mState.mCursorPosition.mLine == lineNo) { 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); @@ -739,21 +976,38 @@ void TextEditor::Render() drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); } - float cx = TextDistanceToLineStart(mState.mCursorPosition); - + // Render the cursor if (focused) { - static auto timeStart = std::chrono::system_clock::now(); - auto timeEnd = std::chrono::system_clock::now(); - auto diff = timeEnd - timeStart; - auto elapsed = std::chrono::duration_cast(diff).count(); + auto timeEnd = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto elapsed = timeEnd - mStartTime; if (elapsed > 400) { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int)line.size()) + { + auto c = line[cindex].mChar; + if (c == '\t') + { + auto x = (1.0f + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + width = x - cx; + } + else + { + char buf2[2]; + buf2[0] = line[cindex].mChar; + buf2[1] = '\0'; + width = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2).x; + } + } ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); - ImVec2 cend(textScreenPos.x + cx + (mOverwrite ? mCharAdvance.x : 1.0f), lineStartScreenPos.y + mCharAdvance.y); + ImVec2 cend(textScreenPos.x + cx + width, lineStartScreenPos.y + mCharAdvance.y); drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); if (elapsed > 800) - timeStart = timeEnd; + mStartTime = timeEnd; } } } @@ -762,32 +1016,68 @@ void TextEditor::Render() auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]); ImVec2 bufferOffset; - for (auto& glyph : line) + for (int i = 0; i < line.size();) { + auto& glyph = line[i]; auto color = GetGlyphColor(glyph); - if ((color != prevColor || glyph.mChar == '\t') && !buffer.empty()) + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && !mLineBuffer.empty()) { const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); - drawList->AddText(newOffset, prevColor, buffer.c_str()); - auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buffer.c_str(), nullptr, nullptr); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), nullptr, nullptr); bufferOffset.x += textSize.x; - buffer.clear(); + mLineBuffer.clear(); } prevColor = color; if (glyph.mChar == '\t') - bufferOffset.x = (1.0f * fontScale + std::floor((1.0f + bufferOffset.x)) / (float(mTabSize) * spaceSize)) * (float(mTabSize) * spaceSize); + { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } + else if (glyph.mChar == ' ') + { + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } else - AppendBuffer(buffer, glyph.mChar, 0); + { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) + mLineBuffer.push_back(line[i++].mChar); + } ++columnNo; } - if (!buffer.empty()) + if (!mLineBuffer.empty()) { const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); - drawList->AddText(newOffset, prevColor, buffer.c_str()); - buffer.clear(); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); } ++lineNo; @@ -837,18 +1127,29 @@ void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) mTextChanged = false; mCursorPositionChanged = false; - ImGui::PushStyleColor(ImGuiCol_ChildWindowBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); - ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); - ImGui::PushAllowKeyboardFocus(true); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) + { + HandleKeyboardInputs(); + ImGui::PushAllowKeyboardFocus(true); + } + + if (mHandleMouseInputs) + HandleMouseInputs(); - HandleKeyboardInputs(); - HandleMouseInputs(); ColorizeInternal(); Render(); - ImGui::PopAllowKeyboardFocus(); - ImGui::EndChild(); + if (mHandleKeyboardInputs) + ImGui::PopAllowKeyboardFocus(); + + if (!mIgnoreImGuiChild) + ImGui::EndChild(); + ImGui::PopStyleVar(); ImGui::PopStyleColor(); @@ -872,7 +1173,7 @@ void TextEditor::SetText(const std::string & aText) mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); } } - + mTextChanged = true; mScrollToTop = true; @@ -913,7 +1214,7 @@ void TextEditor::SetTextLines(const std::vector & aLines) Colorize(); } -void TextEditor::EnterCharacter(Char aChar, bool aShift) +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) { assert(!mReadOnly); @@ -923,23 +1224,25 @@ void TextEditor::EnterCharacter(Char aChar, bool aShift) if (HasSelection()) { - if (aChar == '\t') + if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) { + 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; - end.mColumn = (int)mLines[end.mLine].size(); - } + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); - if (end.mColumn >= (int)mLines[end.mLine].size()) - end.mColumn = (int)mLines[end.mLine].size() - 1; + //if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; u.mRemovedStart = start; u.mRemovedEnd = end; @@ -952,52 +1255,62 @@ void TextEditor::EnterCharacter(Char aChar, bool aShift) auto& line = mLines[i]; if (aShift) { - if (line.empty() == false) + if (!line.empty()) { if (line.front().mChar == '\t') { line.erase(line.begin()); - if (i == end.mLine && end.mColumn > 0) - end.mColumn--; modified = true; } - } - else - { - for (int j = 0; j < mTabSize && line.empty() == false && line.front().mChar == ' '; j++) + else { - line.erase(line.begin()); - if (i == end.mLine && end.mColumn > 0) - end.mColumn--; - modified = true; + for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + { + line.erase(line.begin()); + modified = true; + } } } } else { line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); - if (i == end.mLine) - ++end.mColumn; modified = true; } } if (modified) { - assert(mLines.size() > start.mLine && mLines[start.mLine].size() > start.mColumn); - assert(mLines.size() > end.mLine && mLines[end.mLine].size() > end.mColumn); + 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 + { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + u.mAddedStart = start; - u.mAddedEnd = end; - u.mAdded = GetText(start, end); + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); mTextChanged = true; - AddUndo(u); EnsureCursorVisible(); } return; - } + } // c == '\t' else { u.mRemoved = GetSelectedText(); @@ -1005,7 +1318,7 @@ void TextEditor::EnterCharacter(Char aChar, bool aShift) u.mRemovedEnd = mState.mSelectionEnd; DeleteSelection(); } - } + } // HasSelection auto coord = GetActualCursorCoordinates(); u.mAddedStart = coord; @@ -1019,29 +1332,52 @@ void TextEditor::EnterCharacter(Char aChar, bool aShift) auto& newLine = mLines[coord.mLine + 1]; if (mLanguageDefinition.mAutoIndentation) - { - for (size_t it = 0; it < line.size() && isblank(line[it].mChar); ++it) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) newLine.push_back(line[it]); - } const size_t whitespaceSize = newLine.size(); - newLine.insert(newLine.end(), line.begin() + coord.mColumn, line.end()); - line.erase(line.begin() + coord.mColumn, line.begin() + line.size()); - SetCursorPosition(Coordinates(coord.mLine + 1, (int)whitespaceSize)); + 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 { - auto& line = mLines[coord.mLine]; - if (mOverwrite && (int)line.size() > coord.mColumn) - line[coord.mColumn] = Glyph(aChar, PaletteIndex::Default); + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) + { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < (int)line.size()) + { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } else - line.insert(line.begin() + coord.mColumn, Glyph(aChar, PaletteIndex::Default)); - SetCursorPosition(Coordinates(coord.mLine, coord.mColumn + 1)); + return; } mTextChanged = true; - u.mAdded = aChar; u.mAddedEnd = GetActualCursorCoordinates(); u.mAfter = mState; @@ -1056,6 +1392,11 @@ void TextEditor::SetReadOnly(bool aValue) mReadOnly = aValue; } +void TextEditor::SetColorizerEnable(bool aValue) +{ + mColorizerEnabled = aValue; +} + void TextEditor::SetCursorPosition(const Coordinates & aPosition) { if (mState.mCursorPosition != aPosition) @@ -1087,7 +1428,7 @@ void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aE mState.mSelectionStart = SanitizeCoordinates(aStart); mState.mSelectionEnd = SanitizeCoordinates(aEnd); - if (aStart > aEnd) + if (mState.mSelectionStart > mState.mSelectionEnd) std::swap(mState.mSelectionStart, mState.mSelectionEnd); switch (aMode) @@ -1106,7 +1447,7 @@ void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aE const auto lineNo = mState.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, (int)lineSize); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); break; } default: @@ -1118,6 +1459,11 @@ void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aE mCursorPositionChanged = true; } +void TextEditor::SetTabSize(int aValue) +{ + mTabSize = std::max(0, std::min(32, aValue)); +} + void TextEditor::InsertText(const std::string & aValue) { InsertText(aValue.c_str()); @@ -1207,6 +1553,11 @@ void TextEditor::MoveDown(int aAmount, bool aSelect) } } +static bool IsUTFSequence(char c) +{ + return (c & 0xC0) == 0x80; +} + void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) { if (mLines.empty()) @@ -1214,25 +1565,45 @@ void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) auto oldPos = mState.mCursorPosition; mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); while (aAmount-- > 0) { - if (mState.mCursorPosition.mColumn == 0) + if (cindex == 0) { - if (mState.mCursorPosition.mLine > 0) + if (line > 0) { - --mState.mCursorPosition.mLine; - mState.mCursorPosition.mColumn = (int)mLines[mState.mCursorPosition.mLine].size(); + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; } } else { - mState.mCursorPosition.mColumn = std::max(0, mState.mCursorPosition.mColumn - 1); - if (aWordMode) - mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + --cindex; + if (cindex > 0) + { + 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.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + assert(mState.mCursorPosition.mColumn >= 0); if (aSelect) { @@ -1257,25 +1628,31 @@ void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) { auto oldPos = mState.mCursorPosition; - if (mLines.empty()) + if (mLines.empty() || oldPos.mLine >= mLines.size()) return; + auto cindex = GetCharacterIndex(mState.mCursorPosition); while (aAmount-- > 0) { - auto& line = mLines[mState.mCursorPosition.mLine]; - if (mState.mCursorPosition.mColumn >= (int)line.size()) + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) { - if (mState.mCursorPosition.mLine < (int)mLines.size() - 1) + if (mState.mCursorPosition.mLine < mLines.size() - 1) { mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); mState.mCursorPosition.mColumn = 0; } + else + return; } else { - mState.mCursorPosition.mColumn = std::max(0, std::min((int)line.size(), mState.mCursorPosition.mColumn + 1)); + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); if (aWordMode) - mState.mCursorPosition = FindWordEnd(mState.mCursorPosition); + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); } } @@ -1359,7 +1736,7 @@ void TextEditor::MoveHome(bool aSelect) void TextEditor::MoveEnd(bool aSelect) { auto oldPos = mState.mCursorPosition; - SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, (int)mLines[oldPos.mLine].size())); + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); if (mState.mCursorPosition != oldPos) { @@ -1405,7 +1782,7 @@ void TextEditor::Delete() SetCursorPosition(pos); auto& line = mLines[pos.mLine]; - if (pos.mColumn == (int)line.size()) + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) { if (pos.mLine == (int)mLines.size() - 1) return; @@ -1420,11 +1797,14 @@ void TextEditor::Delete() } else { - u.mRemoved = line[pos.mColumn].mChar; + auto cindex = GetCharacterIndex(pos); u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); - line.erase(line.begin() + pos.mColumn); + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + line.erase(line.begin() + cindex); } mTextChanged = true; @@ -1436,7 +1816,7 @@ void TextEditor::Delete() AddUndo(u); } -void TextEditor::BackSpace() +void TextEditor::Backspace() { assert(!mReadOnly); @@ -1465,12 +1845,12 @@ void TextEditor::BackSpace() return; u.mRemoved = '\n'; - u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, (int)mLines[pos.mLine - 1].size()); + u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); Advance(u.mRemovedEnd); auto& line = mLines[mState.mCursorPosition.mLine]; auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; - auto prevSize = (int)prevLine.size(); + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); prevLine.insert(prevLine.end(), line.begin(), line.end()); ErrorMarkers etmp; @@ -1485,14 +1865,23 @@ void TextEditor::BackSpace() 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 (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; - u.mRemoved = line[pos.mColumn - 1].mChar; u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); --u.mRemovedStart.mColumn; - --mState.mCursorPosition.mColumn; - if (mState.mCursorPosition.mColumn < (int)line.size()) - line.erase(line.begin() + mState.mCursorPosition.mColumn); + + while (cindex < line.size() && cend-- > cindex) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } } mTextChanged = true; @@ -1567,6 +1956,9 @@ void TextEditor::Cut() void TextEditor::Paste() { + if (IsReadOnly()) + return; + auto clipText = ImGui::GetClipboardText(); if (clipText != nullptr && strlen(clipText) > 0) { @@ -1594,12 +1986,12 @@ void TextEditor::Paste() bool TextEditor::CanUndo() const { - return mUndoIndex > 0; + return !mReadOnly && mUndoIndex > 0; } bool TextEditor::CanRedo() const { - return mUndoIndex < (int)mUndoBuffer.size(); + return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); } void TextEditor::Undo(int aSteps) @@ -1617,84 +2009,84 @@ 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 - 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 - } }; + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // 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 + } }; return p; } 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) - 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 - } }; + 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) + 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 + } }; return p; } const TextEditor::Palette & TextEditor::GetRetroBluePalette() { const static Palette p = { { - 0xff00ffff, // None - 0xffffff00, // Keyword - 0xff00ff00, // Number - 0xff808000, // String - 0xff808000, // Char literal - 0xffffffff, // Punctuation - 0xff008000, // Preprocessor - 0xff00ffff, // 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, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // 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 + } }; return p; } @@ -1732,8 +2124,10 @@ std::string TextEditor::GetSelectedText() const std::string TextEditor::GetCurrentLineText()const { - auto lineLength = (int)mLines[mState.mCursorPosition.mLine].size(); - return GetText(Coordinates(mState.mCursorPosition.mLine, 0), Coordinates(mState.mCursorPosition.mLine, lineLength)); + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText( + Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); } void TextEditor::ProcessInputs() @@ -1797,7 +2191,7 @@ void TextEditor::ColorizeRange(int aFromLine, int aToLine) if (hasTokenizeResult == false) { // todo : remove - //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); + //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); for (auto& p : mRegexList) { @@ -1857,24 +2251,27 @@ void TextEditor::ColorizeRange(int aFromLine, int aToLine) void TextEditor::ColorizeInternal() { - if (mLines.empty()) + if (mLines.empty() || !mColorizerEnabled) return; if (mCheckComments) { - auto end = Coordinates((int)mLines.size(), 0); - auto commentStart = end; + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; auto withinString = false; auto withinSingleLineComment = false; auto withinPreproc = false; auto firstChar = true; // there is no other non-whitespace characters in the line before auto concatenate = false; // '\' on the very end of the line - - for (auto currentCoord = Coordinates(0, 0); currentCoord < end; Advance(currentCoord)) + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) { - auto& line = mLines[currentCoord.mLine]; + auto& line = mLines[currentLine]; - if (currentCoord.mColumn == 0 && !concatenate) + if (currentIndex == 0 && !concatenate) { withinSingleLineComment = false; withinPreproc = false; @@ -1885,37 +2282,37 @@ void TextEditor::ColorizeInternal() if (!line.empty()) { - auto& g = line[currentCoord.mColumn]; + auto& g = line[currentIndex]; auto c = g.mChar; if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) firstChar = false; - if (currentCoord.mColumn == line.size() - 1 && line[line.size() - 1].mChar == '\\') + if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\') concatenate = true; - bool inComment = commentStart <= currentCoord; + bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); if (withinString) { - line[currentCoord.mColumn].mMultiLineComment = inComment; + line[currentIndex].mMultiLineComment = inComment; if (c == '\"') { - if (currentCoord.mColumn + 1 < (int)line.size() && line[currentCoord.mColumn + 1].mChar == '\"') + if (currentIndex + 1 < (int)line.size() && line[currentIndex + 1].mChar == '\"') { - Advance(currentCoord); - if (currentCoord.mColumn < (int)line.size()) - line[currentCoord.mColumn].mMultiLineComment = inComment; + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; } else withinString = false; } else if (c == '\\') { - Advance(currentCoord); - if (currentCoord.mColumn < (int)line.size()) - line[currentCoord.mColumn].mMultiLineComment = inComment; + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; } } else @@ -1926,34 +2323,54 @@ void TextEditor::ColorizeInternal() if (c == '\"') { withinString = true; - line[currentCoord.mColumn].mMultiLineComment = inComment; + line[currentIndex].mMultiLineComment = inComment; } else { auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; - auto from = line.begin() + currentCoord.mColumn; + auto from = line.begin() + currentIndex; auto& startStr = mLanguageDefinition.mCommentStart; auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + if (singleStartStr.size() > 0 && - currentCoord.mColumn + singleStartStr.size() <= line.size() && + currentIndex + singleStartStr.size() <= line.size() && equals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.size(), pred)) + { withinSingleLineComment = true; - else if (!withinSingleLineComment && currentCoord.mColumn + startStr.size() <= line.size() && + } + else if (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() && equals(startStr.begin(), startStr.end(), from, from + startStr.size(), pred)) - commentStart = currentCoord; + { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } - inComment = commentStart <= currentCoord; + inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); - line[currentCoord.mColumn].mMultiLineComment = inComment; - line[currentCoord.mColumn].mComment = withinSingleLineComment; + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; auto& endStr = mLanguageDefinition.mCommentEnd; - if (currentCoord.mColumn + 1 >= (int)endStr.size() && + if (currentIndex + 1 >= (int)endStr.size() && equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, pred)) - commentStart = end; + { + commentStartIndex = endIndex; + commentStartLine = endLine; + } } } - line[currentCoord.mColumn].mPreprocessor = withinPreproc; + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= (int)line.size()) + { + currentIndex = 0; + ++currentLine; + } + } + else + { + currentIndex = 0; + ++currentLine; } } mCheckComments = false; @@ -1979,19 +2396,24 @@ float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const { auto& line = mLines[aFrom.mLine]; float distance = 0.0f; - auto fontScale = ImGui::GetFontSize() / ImGui::GetFont()->FontSize; float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; - for (size_t it = 0u; it < line.size() && it < (unsigned)aFrom.mColumn; ++it) + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex; ) { if (line[it].mChar == '\t') { - distance = (1.0f * fontScale + std::floor((1.0f + distance)) / (float(mTabSize) * spaceSize)) * (float(mTabSize) * spaceSize); + distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++it; } else { - char tempCString[2]; - tempCString[0] = line[it].mChar; - tempCString[1] = '\0'; + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++) + tempCString[i] = line[it].mChar; + + tempCString[i] = '\0'; distance += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString, nullptr, nullptr).x; } } @@ -2348,7 +2770,7 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus( { paletteIndex = PaletteIndex::Max; - while (in_begin < in_end && isblank(*in_begin)) + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) in_begin++; if (in_begin == in_end) @@ -2535,7 +2957,7 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() { paletteIndex = PaletteIndex::Max; - while (in_begin < in_end && isblank(*in_begin)) + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) in_begin++; if (in_begin == in_end) @@ -2706,10 +3128,10 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", - "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", - "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", - "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", - "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" }; for (auto& k : identifiers) { diff --git a/imguicolortextedit/TextEditor.h b/imguicolortextedit/TextEditor.h index 9e013b1b..cce374fb 100644 --- a/imguicolortextedit/TextEditor.h +++ b/imguicolortextedit/TextEditor.h @@ -8,7 +8,6 @@ #include #include #include - #include "../imgui/imgui.h" namespace tracy @@ -61,7 +60,14 @@ public: , mEnabled(false) {} }; - + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, + // because it is rendered as " ABC" on the screen. struct Coordinates { int mLine, mColumn; @@ -128,8 +134,8 @@ public: typedef std::map ErrorMarkers; typedef std::unordered_set Breakpoints; typedef std::array Palette; - typedef char Char; - + typedef uint8_t Char; + struct Glyph { Char mChar; @@ -138,7 +144,7 @@ public: bool mMultiLineComment : 1; bool mPreprocessor : 1; - Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex), + Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex), mComment(false), mMultiLineComment(false), mPreprocessor(false) {} }; @@ -149,7 +155,7 @@ public: { typedef std::pair TokenRegexString; typedef std::vector TokenRegexStrings; - typedef bool (*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex); + typedef bool(*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex); std::string mName; Keywords mKeywords; @@ -164,12 +170,12 @@ public: TokenRegexStrings mTokenRegexStrings; bool mCaseSensitive; - + LanguageDefinition() : mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true) { } - + static const LanguageDefinition& CPlusPlus(); static const LanguageDefinition& HLSL(); static const LanguageDefinition& GLSL(); @@ -193,12 +199,14 @@ public: void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); void SetText(const std::string& aText); - void SetTextLines(const std::vector& aLines); std::string GetText() const; + + void SetTextLines(const std::vector& aLines); std::vector GetTextLines() const; + std::string GetSelectedText() const; std::string GetCurrentLineText()const; - + int GetTotalLines() const { return (int)mLines.size(); } bool IsOverwrite() const { return mOverwrite; } @@ -207,9 +215,27 @@ public: bool IsTextChanged() const { return mTextChanged; } bool IsCursorPositionChanged() const { return mCursorPositionChanged; } + bool IsColorizerEnabled() const { return mColorizerEnabled; } + void SetColorizerEnable(bool aValue); + Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); } void SetCursorPosition(const Coordinates& aPosition); + inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;} + inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; } + + inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;} + inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; } + + inline void SetImGuiChildIgnored (bool aValue){ mIgnoreImGuiChild = aValue;} + inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; } + + inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; } + inline bool IsShowingWhitespaces() const { return mShowWhitespaces; } + + void SetTabSize(int aValue); + inline int GetTabSize() const { return mTabSize; } + void InsertText(const std::string& aValue); void InsertText(const char* aValue); @@ -261,14 +287,14 @@ private: UndoRecord( const std::string& aAdded, - const TextEditor::Coordinates aAddedStart, - const TextEditor::Coordinates aAddedEnd, - - const std::string& aRemoved, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + const std::string& aRemoved, const TextEditor::Coordinates aRemovedStart, const TextEditor::Coordinates aRemovedEnd, - - TextEditor::EditorState& aBefore, + + TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter); void Undo(TextEditor* aEditor); @@ -295,7 +321,6 @@ private: float TextDistanceToLineStart(const Coordinates& aFrom) const; void EnsureCursorVisible(); int GetPageSize() const; - int AppendBuffer(std::string& aBuffer, char chr, int aIndex); std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; Coordinates GetActualCursorCoordinates() const; Coordinates SanitizeCoordinates(const Coordinates& aValue) const; @@ -306,12 +331,17 @@ private: Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; Coordinates FindWordStart(const Coordinates& aFrom) const; Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) 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 RemoveLine(int aIndex); Line& InsertLine(int aIndex); - void EnterCharacter(Char aChar, bool aShift); - void BackSpace(); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); void DeleteSelection(); std::string GetWordUnderCursor() const; std::string GetWordAt(const Coordinates& aCoords) const; @@ -326,7 +356,7 @@ private: EditorState mState; UndoBuffer mUndoBuffer; int mUndoIndex; - + int mTabSize; bool mOverwrite; bool mReadOnly; @@ -334,11 +364,16 @@ private: bool mScrollToCursor; bool mScrollToTop; bool mTextChanged; - float mTextStart; // position (in pixels) where a code line starts relative to the left of the 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; + bool mHandleMouseInputs; + bool mIgnoreImGuiChild; + bool mShowWhitespaces; Palette mPaletteBase; Palette mPalette; @@ -350,7 +385,9 @@ private: ErrorMarkers mErrorMarkers; ImVec2 mCharAdvance; Coordinates mInteractiveStart, mInteractiveEnd; - + std::string mLineBuffer; + uint64_t mStartTime; + float mLastClick; };