#include #include #include #include #include #include "TextEditor.h" #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui.h" // for imGui::GetCurrentWindow() // TODO // - multiline comments vs single-line: latter is blocking start of a ML template bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p) { for (; first1 != last1 && first2 != last2; ++first1, ++first2) { if (!p(*first1, *first2)) return false; } return first1 == last1 && first2 == last2; } TextEditor::TextEditor() : mLineSpacing(1.0f) , mUndoIndex(0) , mTabSize(4) , mOverwrite(false) , mReadOnly(false) , mWithinRender(false) , 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(false) , mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) { SetPalette(GetDarkPalette()); SetLanguageDefinition(LanguageDefinition::CPlusPlus()); mLines.push_back(Line()); } TextEditor::~TextEditor() { } void TextEditor::SetLanguageDefinition(const LanguageDefinition & aLanguageDef) { mLanguageDefinition = aLanguageDef; mRegexList.clear(); 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) { mPaletteBase = aValue; } std::string TextEditor::GetText(const Coordinates & aStart, const Coordinates & aEnd) const { std::string result; auto lstart = aStart.mLine; auto lend = aEnd.mLine; auto istart = GetCharacterIndex(aStart); auto iend = GetCharacterIndex(aEnd); size_t s = 0; 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; auto& line = mLines[lstart]; if (istart < (int)line.size()) { result += line[istart].mChar; istart++; } else { istart = 0; ++lstart; result += '\n'; } } return result; } TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const { return SanitizeCoordinates(mState.mCursorPosition); } TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aValue) const { auto line = aValue.mLine; auto column = aValue.mColumn; if (line >= (int)mLines.size()) { if (mLines.empty()) { line = 0; column = 0; } else { line = (int)mLines.size() - 1; column = GetLineMaxColumn(line); } return Coordinates(line, column); } else { column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); 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 { if (aCoordinates.mLine < (int)mLines.size()) { auto& line = mLines[aCoordinates.mLine]; auto cindex = GetCharacterIndex(aCoordinates); if (cindex + 1 < (int)line.size()) { auto delta = UTF8CharLength(line[cindex].mChar); cindex = std::min(cindex + delta, (int)line.size() - 1); } else { ++aCoordinates.mLine; cindex = 0; } aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); } } void TextEditor::DeleteRange(const Coordinates & aStart, const Coordinates & aEnd) { 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]; auto n = GetLineMaxColumn(aStart.mLine); if (aEnd.mColumn >= n) line.erase(line.begin() + start, line.end()); else line.erase(line.begin() + start, line.begin() + end); } else { 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()); if (aStart.mLine < aEnd.mLine) RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); } mTextChanged = true; } int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValue) { assert(!mReadOnly); int cindex = GetCharacterIndex(aWhere); int totalLines = 0; while (*aValue != '\0') { assert(!mLines.empty()); if (*aValue == '\r') { // skip ++aValue; } else if (*aValue == '\n') { if (cindex < (int)mLines[aWhere.mLine].size()) { 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()); } else { InsertLine(aWhere.mLine + 1); } ++aWhere.mLine; aWhere.mColumn = 0; cindex = 0; ++totalLines; ++aValue; } else { auto& line = mLines[aWhere.mLine]; auto d = UTF8CharLength(*aValue); while (d-- > 0 && *aValue != '\0') line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); ++aWhere.mColumn; } mTextChanged = true; } return totalLines; } 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((size_t)(mUndoIndex + 1)); mUndoBuffer.back() = aValue; ++mUndoIndex; } TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const { ImVec2 origin = ImGui::GetCursorScreenPos(); ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); int columnCoord = 0; if (lineNo >= 0 && lineNo < (int)mLines.size()) { auto& line = mLines.at(lineNo); int columnIndex = 0; float columnX = 0.0f; 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)); } TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates & aFrom) const { Coordinates at = aFrom; if (at.mLine >= (int)mLines.size()) return at; auto& line = mLines[at.mLine]; auto cindex = GetCharacterIndex(at); if (cindex >= (int)line.size()) return at; while (cindex > 0 && isspace(line[cindex].mChar)) --cindex; auto cstart = (PaletteIndex)line[cindex].mColorIndex; while (cindex > 0) { 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 Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); } TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates & aFrom) const { Coordinates at = aFrom; if (at.mLine >= (int)mLines.size()) return at; auto& line = mLines[at.mLine]; auto cindex = GetCharacterIndex(at); if (cindex >= (int)line.size()) return at; bool prevspace = (bool)isspace(line[cindex].mChar); auto cstart = (PaletteIndex)line[cindex].mColorIndex; while (cindex < (int)line.size()) { auto c = line[cindex].mChar; auto d = UTF8CharLength(c); if (cstart != (PaletteIndex)line[cindex].mColorIndex) break; 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]; auto cindex = GetCharacterIndex(aAt); if (cindex >= (int)line.size()) return true; 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) { assert(!mReadOnly); assert(aEnd >= aStart); assert(mLines.size() > (size_t)(aEnd - aStart)); ErrorMarkers etmp; for (auto& i : mErrorMarkers) { ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); if (e.first >= aStart && e.first <= aEnd) continue; etmp.insert(e); } mErrorMarkers = std::move(etmp); Breakpoints btmp; for (auto i : mBreakpoints) { if (i >= aStart && i <= aEnd) continue; btmp.insert(i >= aStart ? i - 1 : i); } mBreakpoints = std::move(btmp); mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); assert(!mLines.empty()); mTextChanged = true; } void TextEditor::RemoveLine(int aIndex) { assert(!mReadOnly); assert(mLines.size() > 1); ErrorMarkers etmp; for (auto& i : mErrorMarkers) { ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); if (e.first - 1 == aIndex) continue; etmp.insert(e); } mErrorMarkers = std::move(etmp); Breakpoints btmp; for (auto i : mBreakpoints) { if (i == aIndex) continue; btmp.insert(i >= aIndex ? i - 1 : i); } mBreakpoints = std::move(btmp); mLines.erase(mLines.begin() + aIndex); assert(!mLines.empty()); mTextChanged = true; } TextEditor::Line& TextEditor::InsertLine(int aIndex) { assert(!mReadOnly); auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); ErrorMarkers etmp; for (auto& i : mErrorMarkers) etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); mErrorMarkers = std::move(etmp); Breakpoints btmp; for (auto i : mBreakpoints) btmp.insert(i >= aIndex ? i + 1 : i); mBreakpoints = std::move(btmp); return result; } std::string TextEditor::GetWordUnderCursor() const { auto c = GetCursorPosition(); return GetWordAt(c); } std::string TextEditor::GetWordAt(const Coordinates & aCoords) const { auto start = FindWordStart(aCoords); auto end = FindWordEnd(aCoords); std::string r; 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) return mPalette[(int)PaletteIndex::MultiLineComment]; auto const color = mPalette[(int)aGlyph.mColorIndex]; if (aGlyph.mPreprocessor) { const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor]; const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); } return color; } void TextEditor::HandleKeyboardInputs() { ImGuiIO& io = ImGui::GetIO(); auto shift = io.KeyShift; auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; if (ImGui::IsWindowFocused()) { if (ImGui::IsWindowHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); //ImGui::CaptureKeyboardFromApp(true); io.WantCaptureKeyboard = true; io.WantTextInput = true; if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z))) Undo(); else if (!IsReadOnly() && !ctrl && !shift && alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace))) Undo(); else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Y))) Redo(); else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow))) MoveUp(1, shift); else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow))) MoveDown(1, shift); else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow))) MoveLeft(1, shift, ctrl); else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow))) MoveRight(1, shift, ctrl); else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageUp))) MoveUp(GetPageSize() - 4, shift); else if (!alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageDown))) MoveDown(GetPageSize() - 4, shift); else if (!alt && ctrl && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home))) MoveTop(shift); else if (ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End))) MoveBottom(shift); else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home))) MoveHome(shift); else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End))) MoveEnd(shift); 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(); else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) mOverwrite ^= true; else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) Copy(); else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) Copy(); else if (!IsReadOnly() && !ctrl && shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert))) Paste(); else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V))) Paste(); else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X))) Cut(); else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) Cut(); else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_A))) SelectAll(); else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter))) EnterCharacter('\n', false); else if (!IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab))) EnterCharacter('\t', shift); if (!IsReadOnly() && !io.InputQueueCharacters.empty()) { for (int i = 0; i < io.InputQueueCharacters.Size; i++) { auto c = io.InputQueueCharacters[i]; if (c != 0 && (c == '\n' || c >= 32)) EnterCharacter(c, shift); } io.InputQueueCharacters.resize(0); } } } void TextEditor::HandleMouseInputs() { ImGuiIO& io = ImGui::GetIO(); auto shift = io.KeyShift; auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; if (ImGui::IsWindowHovered()) { 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); /* Left mouse button triple click */ if (tripleClick) { if (!ctrl) { mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); mSelectionMode = SelectionMode::Line; SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); } mLastClick = -1.0f; } /* Left mouse button double click */ else if (doubleClick) { if (!ctrl) { mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); if (mSelectionMode == SelectionMode::Line) mSelectionMode = SelectionMode::Normal; else mSelectionMode = SelectionMode::Word; SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); } mLastClick = (float)ImGui::GetTime(); } /* Left mouse button click */ else if (click) { mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); if (ctrl) mSelectionMode = SelectionMode::Word; else mSelectionMode = SelectionMode::Normal; SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); mLastClick = (float)ImGui::GetTime(); } // Mouse left button dragging (=> update selection) else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) { io.WantCaptureMouse = true; mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); } } } } 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); /* Update palette with the current alpha from style */ for (int i = 0; i < (int)PaletteIndex::Max; ++i) { auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); color.w *= ImGui::GetStyle().Alpha; mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); } assert(mLineBuffer.empty()); auto contentSize = ImGui::GetWindowContentRegionMax(); auto drawList = ImGui::GetWindowDrawList(); float longest(mTextStart); if (mScrollToTop) { mScrollToTop = false; ImGui::SetScrollY(0.f); } ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); auto scrollX = ImGui::GetScrollX(); auto scrollY = ImGui::GetScrollY(); auto lineNo = (int)floor(scrollY / mCharAdvance.y); auto globalLineMax = (int)mLines.size(); auto lineMax = std::max(0, std::min((int)mLines.size() - 1, lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y))); // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width char buf[16]; snprintf(buf, 16, " %d ", globalLineMax); mTextStart = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x + mLeftMargin; if (!mLines.empty()) { float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; while (lineNo <= lineMax) { ImVec2 lineStartScreenPos = ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y); ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); auto& line = mLines[lineNo]; longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest); auto columnNo = 0; Coordinates lineStartCoord(lineNo, 0); Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); // Draw selection for the current line 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); if (mState.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]); } // Draw breakpoints auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); if (mBreakpoints.count(lineNo + 1) != 0) { auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::Breakpoint]); } // Draw error markers auto errorIt = mErrorMarkers.find(lineNo + 1); if (errorIt != mErrorMarkers.end()) { auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]); if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) { ImGui::BeginTooltip(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); ImGui::Text("Error at line %d:", errorIt->first); ImGui::PopStyleColor(); ImGui::Separator(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); ImGui::Text("%s", errorIt->second.c_str()); ImGui::PopStyleColor(); ImGui::EndTooltip(); } } // 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); 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); drawList->AddRectFilled(start, end, mPalette[(int)(focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]); drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); } // Render the cursor if (focused) { 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 + width, lineStartScreenPos.y + mCharAdvance.y); drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); if (elapsed > 800) mStartTime = timeEnd; } } } // Render colorized text auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]); ImVec2 bufferOffset; for (int i = 0; i < line.size();) { auto& glyph = line[i]; auto color = GetGlyphColor(glyph); 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, mLineBuffer.c_str()); auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), nullptr, nullptr); bufferOffset.x += textSize.x; mLineBuffer.clear(); } prevColor = color; if (glyph.mChar == '\t') { 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 { auto l = UTF8CharLength(glyph.mChar); while (l-- > 0) mLineBuffer.push_back(line[i++].mChar); } ++columnNo; } if (!mLineBuffer.empty()) { const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); mLineBuffer.clear(); } ++lineNo; } // Draw a tooltip on known identifiers/preprocessor symbols if (ImGui::IsMousePosValid()) { auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); if (!id.empty()) { auto it = mLanguageDefinition.mIdentifiers.find(id); if (it != mLanguageDefinition.mIdentifiers.end()) { ImGui::BeginTooltip(); ImGui::TextUnformatted(it->second.mDeclaration.c_str()); ImGui::EndTooltip(); } else { auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) { ImGui::BeginTooltip(); ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); ImGui::EndTooltip(); } } } } } 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) { mWithinRender = true; mTextChanged = false; mCursorPositionChanged = false; 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_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); if (mHandleKeyboardInputs) { HandleKeyboardInputs(); ImGui::PushAllowKeyboardFocus(true); } if (mHandleMouseInputs) HandleMouseInputs(); ColorizeInternal(); Render(); if (mHandleKeyboardInputs) ImGui::PopAllowKeyboardFocus(); if (!mIgnoreImGuiChild) ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); mWithinRender = false; } void TextEditor::SetText(const std::string & aText) { mLines.clear(); mLines.emplace_back(Line()); for (auto chr : aText) { if (chr == '\r') { // ignore the carriage return character } else if (chr == '\n') mLines.emplace_back(Line()); else { mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); } } mTextChanged = true; mScrollToTop = true; mUndoBuffer.clear(); mUndoIndex = 0; Colorize(); } void TextEditor::SetTextLines(const std::vector & aLines) { mLines.clear(); if (aLines.empty()) { mLines.emplace_back(Line()); } else { mLines.resize(aLines.size()); for (size_t i = 0; i < aLines.size(); ++i) { const std::string & aLine = aLines[i]; mLines[i].reserve(aLine.size()); for (size_t j = 0; j < aLine.size(); ++j) mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); } } mTextChanged = true; mScrollToTop = true; mUndoBuffer.clear(); mUndoIndex = 0; Colorize(); } void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) { assert(!mReadOnly); UndoRecord u; u.mBefore = mState; if (HasSelection()) { 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; 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; u.mRemovedStart = start; u.mRemovedEnd = end; u.mRemoved = GetText(start, end); bool modified = false; for (int i = start.mLine; i <= end.mLine; i++) { auto& line = mLines[i]; if (aShift) { if (!line.empty()) { if (line.front().mChar == '\t') { line.erase(line.begin()); modified = true; } else { 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)); 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 { end = Coordinates(originalEnd.mLine, 0); rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); u.mAdded = GetText(start, rangeEnd); } u.mAddedStart = start; u.mAddedEnd = rangeEnd; u.mAfter = mState; mState.mSelectionStart = start; mState.mSelectionEnd = end; AddUndo(u); mTextChanged = true; EnsureCursorVisible(); } return; } // c == '\t' else { u.mRemoved = GetSelectedText(); u.mRemovedStart = mState.mSelectionStart; u.mRemovedEnd = mState.mSelectionEnd; DeleteSelection(); } } // HasSelection auto coord = GetActualCursorCoordinates(); u.mAddedStart = coord; assert(!mLines.empty()); if (aChar == '\n') { InsertLine(coord.mLine + 1); auto& line = mLines[coord.mLine]; auto& newLine = mLines[coord.mLine + 1]; 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]); 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) { 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 return; } mTextChanged = true; u.mAddedEnd = GetActualCursorCoordinates(); u.mAfter = mState; AddUndo(u); Colorize(coord.mLine - 1, 3); EnsureCursorVisible(); } void TextEditor::SetReadOnly(bool aValue) { mReadOnly = aValue; } void TextEditor::SetColorizerEnable(bool aValue) { mColorizerEnabled = aValue; } void TextEditor::SetCursorPosition(const Coordinates & aPosition) { if (mState.mCursorPosition != aPosition) { mState.mCursorPosition = aPosition; mCursorPositionChanged = true; EnsureCursorVisible(); } } void TextEditor::SetSelectionStart(const Coordinates & aPosition) { mState.mSelectionStart = SanitizeCoordinates(aPosition); if (mState.mSelectionStart > mState.mSelectionEnd) std::swap(mState.mSelectionStart, mState.mSelectionEnd); } void TextEditor::SetSelectionEnd(const Coordinates & aPosition) { mState.mSelectionEnd = SanitizeCoordinates(aPosition); if (mState.mSelectionStart > mState.mSelectionEnd) std::swap(mState.mSelectionStart, mState.mSelectionEnd); } void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aEnd, SelectionMode aMode) { auto oldSelStart = mState.mSelectionStart; auto oldSelEnd = mState.mSelectionEnd; mState.mSelectionStart = SanitizeCoordinates(aStart); mState.mSelectionEnd = SanitizeCoordinates(aEnd); if (mState.mSelectionStart > mState.mSelectionEnd) std::swap(mState.mSelectionStart, mState.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 lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); break; } default: break; } if (mState.mSelectionStart != oldSelStart || mState.mSelectionEnd != oldSelEnd) 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()); } void TextEditor::InsertText(const char * aValue) { if (aValue == nullptr) return; auto pos = GetActualCursorCoordinates(); auto start = std::min(pos, mState.mSelectionStart); int totalLines = pos.mLine - start.mLine; totalLines += InsertTextAt(pos, aValue); SetSelection(pos, pos); SetCursorPosition(pos); Colorize(start.mLine - 1, totalLines + 2); } void TextEditor::DeleteSelection() { assert(mState.mSelectionEnd >= mState.mSelectionStart); if (mState.mSelectionEnd == mState.mSelectionStart) return; DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); SetSelection(mState.mSelectionStart, mState.mSelectionStart); SetCursorPosition(mState.mSelectionStart); Colorize(mState.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) { if (aSelect) { if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else if (oldPos == mInteractiveEnd) mInteractiveEnd = mState.mCursorPosition; else { mInteractiveStart = mState.mCursorPosition; mInteractiveEnd = oldPos; } } else mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; SetSelection(mInteractiveStart, mInteractiveEnd); 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) { if (aSelect) { if (oldPos == mInteractiveEnd) mInteractiveEnd = mState.mCursorPosition; else if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else { mInteractiveStart = oldPos; mInteractiveEnd = mState.mCursorPosition; } } else mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; SetSelection(mInteractiveStart, mInteractiveEnd); EnsureCursorVisible(); } } static bool IsUTFSequence(char c) { return (c & 0xC0) == 0x80; } 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) { if (cindex == 0) { if (line > 0) { --line; if ((int)mLines.size() > line) cindex = (int)mLines[line].size(); else cindex = 0; } } else { --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) { if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else if (oldPos == mInteractiveEnd) mInteractiveEnd = mState.mCursorPosition; else { mInteractiveStart = mState.mCursorPosition; mInteractiveEnd = oldPos; } } 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()) return; auto cindex = GetCharacterIndex(mState.mCursorPosition); while (aAmount-- > 0) { auto lindex = mState.mCursorPosition.mLine; auto& line = mLines[lindex]; if (cindex >= line.size()) { 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 { cindex += UTF8CharLength(line[cindex].mChar); mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); if (aWordMode) mState.mCursorPosition = FindNextWord(mState.mCursorPosition); } } if (aSelect) { if (oldPos == mInteractiveEnd) mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); else if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else { mInteractiveStart = oldPos; mInteractiveEnd = mState.mCursorPosition; } } else mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); EnsureCursorVisible(); } void TextEditor::MoveTop(bool aSelect) { auto oldPos = mState.mCursorPosition; SetCursorPosition(Coordinates(0, 0)); if (mState.mCursorPosition != oldPos) { if (aSelect) { mInteractiveEnd = oldPos; mInteractiveStart = mState.mCursorPosition; } else mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; SetSelection(mInteractiveStart, mInteractiveEnd); } } void TextEditor::TextEditor::MoveBottom(bool aSelect) { auto oldPos = GetCursorPosition(); auto newPos = Coordinates((int)mLines.size() - 1, 0); SetCursorPosition(newPos); if (aSelect) { mInteractiveStart = oldPos; mInteractiveEnd = newPos; } else mInteractiveStart = mInteractiveEnd = newPos; SetSelection(mInteractiveStart, mInteractiveEnd); } void TextEditor::MoveHome(bool aSelect) { auto oldPos = mState.mCursorPosition; SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); if (mState.mCursorPosition != oldPos) { if (aSelect) { if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else if (oldPos == mInteractiveEnd) mInteractiveEnd = mState.mCursorPosition; else { mInteractiveStart = mState.mCursorPosition; mInteractiveEnd = oldPos; } } 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) { if (aSelect) { if (oldPos == mInteractiveEnd) mInteractiveEnd = mState.mCursorPosition; else if (oldPos == mInteractiveStart) mInteractiveStart = mState.mCursorPosition; else { mInteractiveStart = oldPos; mInteractiveEnd = mState.mCursorPosition; } } else mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; SetSelection(mInteractiveStart, mInteractiveEnd); } } void TextEditor::Delete() { assert(!mReadOnly); if (mLines.empty()) return; UndoRecord u; u.mBefore = mState; if (HasSelection()) { u.mRemoved = GetSelectedText(); u.mRemovedStart = mState.mSelectionStart; u.mRemovedEnd = mState.mSelectionEnd; DeleteSelection(); } else { auto pos = GetActualCursorCoordinates(); SetCursorPosition(pos); auto& line = mLines[pos.mLine]; if (pos.mColumn == GetLineMaxColumn(pos.mLine)) { if (pos.mLine == (int)mLines.size() - 1) return; 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); } mTextChanged = true; Colorize(pos.mLine, 1); } u.mAfter = mState; AddUndo(u); } void TextEditor::Backspace() { assert(!mReadOnly); if (mLines.empty()) return; UndoRecord u; u.mBefore = mState; if (HasSelection()) { u.mRemoved = GetSelectedText(); u.mRemovedStart = mState.mSelectionStart; u.mRemovedEnd = mState.mSelectionEnd; DeleteSelection(); } else { auto pos = GetActualCursorCoordinates(); SetCursorPosition(pos); if (mState.mCursorPosition.mColumn == 0) { if (mState.mCursorPosition.mLine == 0) return; u.mRemoved = '\n'; 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 = 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.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); mErrorMarkers = std::move(etmp); 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 (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) // --cindex; u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); --u.mRemovedStart.mColumn; --mState.mCursorPosition.mColumn; while (cindex < line.size() && cend-- > cindex) { u.mRemoved += line[cindex].mChar; line.erase(line.begin() + cindex); } } mTextChanged = true; EnsureCursorVisible(); Colorize(mState.mCursorPosition.mLine, 1); } u.mAfter = mState; AddUndo(u); } void TextEditor::SelectWordUnderCursor() { auto c = GetCursorPosition(); SetSelection(FindWordStart(c), FindWordEnd(c)); } void TextEditor::SelectAll() { SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0)); } bool TextEditor::HasSelection() const { return mState.mSelectionEnd > mState.mSelectionStart; } void TextEditor::Copy() { if (HasSelection()) { ImGui::SetClipboardText(GetSelectedText().c_str()); } else { if (!mLines.empty()) { std::string str; auto& line = mLines[GetActualCursorCoordinates().mLine]; for (auto& g : line) str.push_back(g.mChar); ImGui::SetClipboardText(str.c_str()); } } } void TextEditor::Cut() { if (IsReadOnly()) { Copy(); } else { if (HasSelection()) { UndoRecord u; u.mBefore = mState; u.mRemoved = GetSelectedText(); u.mRemovedStart = mState.mSelectionStart; u.mRemovedEnd = mState.mSelectionEnd; Copy(); DeleteSelection(); u.mAfter = mState; AddUndo(u); } } } void TextEditor::Paste() { if (IsReadOnly()) return; auto clipText = ImGui::GetClipboardText(); if (clipText != nullptr && strlen(clipText) > 0) { UndoRecord u; u.mBefore = mState; if (HasSelection()) { u.mRemoved = GetSelectedText(); u.mRemovedStart = mState.mSelectionStart; u.mRemovedEnd = mState.mSelectionEnd; DeleteSelection(); } u.mAdded = clipText; u.mAddedStart = GetActualCursorCoordinates(); InsertText(clipText); u.mAddedEnd = GetActualCursorCoordinates(); u.mAfter = mState; AddUndo(u); } } bool TextEditor::CanUndo() const { return !mReadOnly && mUndoIndex > 0; } bool TextEditor::CanRedo() const { return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); } void TextEditor::Undo(int aSteps) { while (CanUndo() && aSteps-- > 0) mUndoBuffer[--mUndoIndex].Undo(this); } void TextEditor::Redo(int aSteps) { while (CanRedo() && aSteps-- > 0) mUndoBuffer[mUndoIndex++].Redo(this); } 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 } }; 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 } }; 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 } }; return p; } std::string TextEditor::GetText() const { return GetText(Coordinates(), Coordinates((int)mLines.size(), 0)); } std::vector TextEditor::GetTextLines() const { std::vector result; result.reserve(mLines.size()); for (auto & line : mLines) { std::string text; text.resize(line.size()); for (size_t i = 0; i < line.size(); ++i) text[i] = line[i].mChar; result.emplace_back(std::move(text)); } return result; } std::string TextEditor::GetSelectedText() const { return GetText(mState.mSelectionStart, mState.mSelectionEnd); } std::string TextEditor::GetCurrentLineText()const { auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); return GetText( Coordinates(mState.mCursorPosition.mLine, 0), Coordinates(mState.mCursorPosition.mLine, lineLength)); } void TextEditor::ProcessInputs() { } void TextEditor::Colorize(int aFromLine, int aLines) { int toLine = aLines == -1 ? (int)mLines.size() : std::min((int)mLines.size(), aFromLine + aLines); mColorRangeMin = std::min(mColorRangeMin, aFromLine); mColorRangeMax = std::max(mColorRangeMax, toLine); mColorRangeMin = std::max(0, mColorRangeMin); mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); mCheckComments = true; } void TextEditor::ColorizeRange(int aFromLine, int aToLine) { if (mLines.empty() || aFromLine >= aToLine) return; std::string buffer; std::cmatch results; std::string id; int endLine = std::max(0, std::min((int)mLines.size(), aToLine)); for (int i = aFromLine; i < endLine; ++i) { auto& line = mLines[i]; if (line.empty()) continue; buffer.resize(line.size()); for (size_t j = 0; j < line.size(); ++j) { auto& col = line[j]; buffer[j] = col.mChar; col.mColorIndex = PaletteIndex::Default; } const char * bufferBegin = &buffer.front(); const char * bufferEnd = bufferBegin + buffer.size(); auto last = bufferEnd; for (auto first = bufferBegin; first != last; ) { const char * token_begin = nullptr; const char * token_end = nullptr; PaletteIndex token_color = PaletteIndex::Default; bool hasTokenizeResult = false; if (mLanguageDefinition.mTokenize != nullptr) { if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color)) hasTokenizeResult = true; } if (hasTokenizeResult == false) { // todo : remove //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); for (auto& p : mRegexList) { if (std::regex_search(first, last, results, p.first, std::regex_constants::match_continuous)) { hasTokenizeResult = true; auto& v = *results.begin(); token_begin = v.first; token_end = v.second; token_color = p.second; break; } } } if (hasTokenizeResult == false) { first++; } else { const size_t token_length = token_end - token_begin; if (token_color == PaletteIndex::Identifier) { id.assign(token_begin, token_end); // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? if (!mLanguageDefinition.mCaseSensitive) std::transform(id.begin(), id.end(), id.begin(), ::toupper); if (!line[first - bufferBegin].mPreprocessor) { if (mLanguageDefinition.mKeywords.count(id) != 0) token_color = PaletteIndex::Keyword; else if (mLanguageDefinition.mIdentifiers.count(id) != 0) token_color = PaletteIndex::KnownIdentifier; else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) token_color = PaletteIndex::PreprocIdentifier; } else { if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) token_color = PaletteIndex::PreprocIdentifier; } } for (size_t j = 0; j < token_length; ++j) line[(token_begin - bufferBegin) + j].mColorIndex = token_color; first = token_end; } } } } void TextEditor::ColorizeInternal() { if (mLines.empty() || !mColorizerEnabled) return; if (mCheckComments) { 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 auto currentLine = 0; auto currentIndex = 0; while (currentLine < endLine || currentIndex < endIndex) { auto& line = mLines[currentLine]; if (currentIndex == 0 && !concatenate) { withinSingleLineComment = false; withinPreproc = false; firstChar = true; } concatenate = false; if (!line.empty()) { auto& g = line[currentIndex]; auto c = g.mChar; if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) firstChar = false; if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\') concatenate = true; bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); if (withinString) { line[currentIndex].mMultiLineComment = inComment; if (c == '\"') { if (currentIndex + 1 < (int)line.size() && line[currentIndex + 1].mChar == '\"') { currentIndex += 1; if (currentIndex < (int)line.size()) line[currentIndex].mMultiLineComment = inComment; } else withinString = false; } else if (c == '\\') { currentIndex += 1; if (currentIndex < (int)line.size()) line[currentIndex].mMultiLineComment = inComment; } } else { if (firstChar && c == mLanguageDefinition.mPreprocChar) withinPreproc = true; if (c == '\"') { withinString = true; line[currentIndex].mMultiLineComment = inComment; } else { auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; auto from = line.begin() + currentIndex; auto& startStr = mLanguageDefinition.mCommentStart; auto& singleStartStr = mLanguageDefinition.mSingleLineComment; if (singleStartStr.size() > 0 && currentIndex + singleStartStr.size() <= line.size() && equals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.size(), pred)) { withinSingleLineComment = true; } else if (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() && equals(startStr.begin(), startStr.end(), from, from + startStr.size(), pred)) { commentStartLine = currentLine; commentStartIndex = currentIndex; } inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); line[currentIndex].mMultiLineComment = inComment; line[currentIndex].mComment = withinSingleLineComment; auto& endStr = mLanguageDefinition.mCommentEnd; if (currentIndex + 1 >= (int)endStr.size() && equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, pred)) { commentStartIndex = endIndex; commentStartLine = endLine; } } } line[currentIndex].mPreprocessor = withinPreproc; currentIndex += UTF8CharLength(c); if (currentIndex >= (int)line.size()) { currentIndex = 0; ++currentLine; } } else { currentIndex = 0; ++currentLine; } } mCheckComments = false; } if (mColorRangeMin < mColorRangeMax) { const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; const int to = std::min(mColorRangeMin + increment, mColorRangeMax); ColorizeRange(mColorRangeMin, to); mColorRangeMin = to; if (mColorRangeMax == mColorRangeMin) { mColorRangeMin = std::numeric_limits::max(); mColorRangeMax = 0; } return; } } float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const { auto& line = mLines[aFrom.mLine]; float distance = 0.0f; float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; int colIndex = GetCharacterIndex(aFrom); for (size_t it = 0u; it < line.size() && it < colIndex; ) { if (line[it].mChar == '\t') { distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); ++it; } else { 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; } } return distance; } void TextEditor::EnsureCursorVisible() { if (!mWithinRender) { mScrollToCursor = true; return; } float scrollX = ImGui::GetScrollX(); float scrollY = ImGui::GetScrollY(); auto height = ImGui::GetWindowHeight(); auto width = ImGui::GetWindowWidth(); auto top = 1 + (int)ceil(scrollY / mCharAdvance.y); auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y); auto left = (int)ceil(scrollX / mCharAdvance.x); auto right = (int)ceil((scrollX + width) / mCharAdvance.x); auto pos = GetActualCursorCoordinates(); auto len = TextDistanceToLineStart(pos); if (pos.mLine < top) ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y)); if (pos.mLine > bottom - 4) ImGui::SetScrollY(std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height)); if (len + mTextStart < left + 4) ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); if (len + mTextStart > right - 4) ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); } int TextEditor::GetPageSize() const { auto height = ImGui::GetWindowHeight() - 20.0f; return (int)floor(height / mCharAdvance.y); } 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, 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); } void TextEditor::UndoRecord::Undo(TextEditor * aEditor) { if (!mAdded.empty()) { aEditor->DeleteRange(mAddedStart, mAddedEnd); aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); } if (!mRemoved.empty()) { auto start = mRemovedStart; aEditor->InsertTextAt(start, mRemoved.c_str()); aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); } aEditor->mState = mBefore; aEditor->EnsureCursorVisible(); } void TextEditor::UndoRecord::Redo(TextEditor * aEditor) { if (!mRemoved.empty()) { aEditor->DeleteRange(mRemovedStart, mRemovedEnd); aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); } if (!mAdded.empty()) { auto start = mAddedStart; aEditor->InsertTextAt(start, mAdded.c_str()); aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); } aEditor->mState = mAfter; aEditor->EnsureCursorVisible(); } static bool TokenizeCStyleString(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) { const char * p = in_begin; if (*p == '"') { p++; while (p < in_end) { // handle end of string if (*p == '"') { out_begin = in_begin; out_end = p + 1; return true; } // handle escape character for " if (*p == '\\' && p + 1 < in_end && p[1] == '"') p++; p++; } } return false; } static bool TokenizeCStyleCharacterLiteral(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) { const char * p = in_begin; if (*p == '\'') { p++; // handle escape characters if (p < in_end && *p == '\\') p++; if (p < in_end) p++; // handle end of character literal if (p < in_end && *p == '\'') { out_begin = in_begin; out_end = p + 1; return true; } } return false; } static bool TokenizeCStyleIdentifier(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) { const char * p = in_begin; if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') { p++; while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) p++; out_begin = in_begin; out_end = p; return true; } return false; } static bool TokenizeCStyleNumber(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) { const char * p = in_begin; const bool startsWithNumber = *p >= '0' && *p <= '9'; if (*p != '+' && *p != '-' && !startsWithNumber) return false; p++; bool hasNumber = startsWithNumber; while (p < in_end && (*p >= '0' && *p <= '9')) { hasNumber = true; p++; } if (hasNumber == false) return false; bool isFloat = false; bool isHex = false; bool isBinary = false; if (p < in_end) { if (*p == '.') { isFloat = true; p++; while (p < in_end && (*p >= '0' && *p <= '9')) p++; } else if (*p == 'x' || *p == 'X') { // hex formatted integer of the type 0xef80 isHex = true; p++; while (p < in_end && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) p++; } else if (*p == 'b' || *p == 'B') { // binary formatted integer of the type 0b01011101 isBinary = true; p++; while (p < in_end && (*p >= '0' && *p <= '1')) p++; } } if (isHex == false && isBinary == false) { // floating point exponent if (p < in_end && (*p == 'e' || *p == 'E')) { isFloat = true; p++; if (p < in_end && (*p == '+' || *p == '-')) p++; bool hasDigits = false; while (p < in_end && (*p >= '0' && *p <= '9')) { hasDigits = true; p++; } if (hasDigits == false) return false; } // single precision floating point type if (p < in_end && *p == 'f') p++; } if (isFloat == false) { // integer size type while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) p++; } out_begin = in_begin; out_end = p; return true; } static bool TokenizeCStylePunctuation(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) { (void)in_end; switch (*in_begin) { case '[': case ']': case '{': case '}': case '!': case '%': case '^': case '&': case '*': case '(': case ')': case '-': case '+': case '=': case '~': case '|': case '<': case '>': case '?': case ':': case '/': case ';': case ',': case '.': out_begin = in_begin; out_end = in_begin + 1; return true; } return false; } const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus() { static bool inited = false; static LanguageDefinition langDef; if (!inited) { static const char* const cppKeywords[] = { "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", "compl", "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" }; for (auto& k : cppKeywords) langDef.mKeywords.insert(k); static const char* const identifiers[] = { "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", "max" }; for (auto& k : identifiers) { Identifier id; id.mDeclaration = "Built-in function"; langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); } langDef.mTokenize = [](const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex) -> bool { paletteIndex = PaletteIndex::Max; while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) in_begin++; if (in_begin == in_end) { out_begin = in_end; out_end = in_end; paletteIndex = PaletteIndex::Default; } else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) paletteIndex = PaletteIndex::String; else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) paletteIndex = PaletteIndex::CharLiteral; else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) paletteIndex = PaletteIndex::Identifier; else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) paletteIndex = PaletteIndex::Number; else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) paletteIndex = PaletteIndex::Punctuation; return paletteIndex != PaletteIndex::Max; }; langDef.mCommentStart = "/*"; langDef.mCommentEnd = "*/"; langDef.mSingleLineComment = "//"; langDef.mCaseSensitive = true; langDef.mAutoIndentation = true; langDef.mName = "C++"; inited = true; } return langDef; }