| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969 |
- #include <algorithm>
- #include <string>
- #include <cmath>
- #include <set>
- #include "TextEditor.h"
- #define IMGUI_SCROLLBAR_WIDTH 14.0f
- #define POS_TO_COORDS_COLUMN_OFFSET 0.33f
- #define IMGUI_DEFINE_MATH_OPERATORS
- #include "imgui.h" // for imGui::GetCurrentWindow()
- // --------------------------------------- //
- // ------------- Exposed API ------------- //
- TextEditor::TextEditor()
- {
- SetPalette(defaultPalette);
- mLines.push_back(Line());
- }
- TextEditor::~TextEditor()
- {
- }
- void TextEditor::SetPalette(PaletteId aValue)
- {
- mPaletteId = aValue;
- const Palette* palletteBase;
- switch (mPaletteId)
- {
- case PaletteId::Dark:
- default:
- palletteBase = &(GetDarkPalette());
- break;
- case PaletteId::Light:
- palletteBase = &(GetLightPalette());
- break;
- case PaletteId::Mariana:
- palletteBase = &(GetMarianaPalette());
- break;
- case PaletteId::RetroBlue:
- palletteBase = &(GetRetroBluePalette());
- break;
- }
- /* Update palette with the current alpha from style */
- for (int i = 0; i < (int)PaletteIndex::Max; ++i)
- {
- ImVec4 color = U32ColorToVec4((*palletteBase)[i]);
- color.w *= ImGui::GetStyle().Alpha;
- mPalette[i] = ImGui::ColorConvertFloat4ToU32(color);
- }
- }
- void TextEditor::SetLanguageDefinition(LanguageDefinitionId aValue)
- {
- mLanguageDefinitionId = aValue;
- switch (mLanguageDefinitionId)
- {
- case LanguageDefinitionId::None:
- mLanguageDefinition = nullptr;
- return;
- case LanguageDefinitionId::Cpp:
- mLanguageDefinition = &(LanguageDefinition::Cpp());
- break;
- case LanguageDefinitionId::C:
- mLanguageDefinition = &(LanguageDefinition::C());
- break;
- case LanguageDefinitionId::Cs:
- mLanguageDefinition = &(LanguageDefinition::Cs());
- break;
- case LanguageDefinitionId::Python:
- mLanguageDefinition = &(LanguageDefinition::Python());
- break;
- case LanguageDefinitionId::Lua:
- mLanguageDefinition = &(LanguageDefinition::Lua());
- break;
- case LanguageDefinitionId::Json:
- mLanguageDefinition = &(LanguageDefinition::Json());
- break;
- case LanguageDefinitionId::Sql:
- mLanguageDefinition = &(LanguageDefinition::Sql());
- break;
- case LanguageDefinitionId::AngelScript:
- mLanguageDefinition = &(LanguageDefinition::AngelScript());
- break;
- case LanguageDefinitionId::Glsl:
- mLanguageDefinition = &(LanguageDefinition::Glsl());
- break;
- case LanguageDefinitionId::Hlsl:
- mLanguageDefinition = &(LanguageDefinition::Hlsl());
- break;
- }
- mRegexList.clear();
- for (const auto& r : mLanguageDefinition->mTokenRegexStrings)
- mRegexList.push_back(std::make_pair(boost::regex(r.first, boost::regex_constants::optimize), r.second));
- Colorize();
- }
- const char* TextEditor::GetLanguageDefinitionName() const
- {
- return mLanguageDefinition != nullptr ? mLanguageDefinition->mName.c_str() : "None";
- }
- void TextEditor::SetTabSize(int aValue)
- {
- mTabSize = Max(1, Min(8, aValue));
- }
- void TextEditor::SetLineSpacing(float aValue)
- {
- mLineSpacing = Max(1.0f, Min(2.0f, aValue));
- }
- void TextEditor::SelectAll()
- {
- ClearSelections();
- ClearExtraCursors();
- MoveTop();
- MoveBottom(true);
- }
- void TextEditor::SelectLine(int aLine)
- {
- ClearSelections();
- ClearExtraCursors();
- SetSelection({ aLine, 0 }, { aLine, GetLineMaxColumn(aLine) });
- }
- void TextEditor::SelectRegion(int aStartLine, int aStartChar, int aEndLine, int aEndChar)
- {
- ClearSelections();
- ClearExtraCursors();
- SetSelection(aStartLine, aStartChar, aEndLine, aEndChar);
- }
- void TextEditor::SelectNextOccurrenceOf(const char* aText, int aTextSize, bool aCaseSensitive)
- {
- ClearSelections();
- ClearExtraCursors();
- SelectNextOccurrenceOf(aText, aTextSize, -1, aCaseSensitive);
- }
- void TextEditor::SelectAllOccurrencesOf(const char* aText, int aTextSize, bool aCaseSensitive)
- {
- ClearSelections();
- ClearExtraCursors();
- SelectNextOccurrenceOf(aText, aTextSize, -1, aCaseSensitive);
- Coordinates startPos = mState.mCursors[mState.GetLastAddedCursorIndex()].mInteractiveEnd;
- while (true)
- {
- AddCursorForNextOccurrence(aCaseSensitive);
- Coordinates lastAddedPos = mState.mCursors[mState.GetLastAddedCursorIndex()].mInteractiveEnd;
- if (lastAddedPos == startPos)
- break;
- }
- }
- bool TextEditor::AnyCursorHasSelection() const
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- if (mState.mCursors[c].HasSelection())
- return true;
- return false;
- }
- bool TextEditor::AllCursorsHaveSelection() const
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- if (!mState.mCursors[c].HasSelection())
- return false;
- return true;
- }
- void TextEditor::ClearExtraCursors()
- {
- mState.mCurrentCursor = 0;
- }
- void TextEditor::ClearSelections()
- {
- for (int c = mState.mCurrentCursor; c > -1; c--)
- mState.mCursors[c].mInteractiveEnd =
- mState.mCursors[c].mInteractiveStart =
- mState.mCursors[c].GetSelectionEnd();
- }
- void TextEditor::SetCursorPosition(int aLine, int aCharIndex)
- {
- SetCursorPosition({ aLine, GetCharacterColumn(aLine, aCharIndex) }, -1, true);
- }
- int TextEditor::GetFirstVisibleLine()
- {
- return mFirstVisibleLine;
- }
- int TextEditor::GetLastVisibleLine()
- {
- return mLastVisibleLine;
- }
- void TextEditor::SetViewAtLine(int aLine, SetViewAtLineMode aMode)
- {
- mSetViewAtLine = aLine;
- mSetViewAtLineMode = aMode;
- }
- void TextEditor::Copy()
- {
- if (AnyCursorHasSelection())
- {
- std::string clipboardText = GetClipboardText();
- ImGui::SetClipboardText(clipboardText.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 (mReadOnly)
- {
- Copy();
- }
- else
- {
- if (AnyCursorHasSelection())
- {
- UndoRecord u;
- u.mBefore = mState;
- Copy();
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- u.mOperations.push_back({ GetSelectedText(c), mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd(), UndoOperationType::Delete });
- DeleteSelection(c);
- }
- u.mAfter = mState;
- AddUndo(u);
- }
- }
- }
- void TextEditor::Paste()
- {
- if (mReadOnly)
- return;
- // check if we should do multicursor paste
- std::string clipText = ImGui::GetClipboardText();
- bool canPasteToMultipleCursors = false;
- std::vector<std::pair<int, int>> clipTextLines;
- if (mState.mCurrentCursor > 0)
- {
- clipTextLines.push_back({ 0,0 });
- for (int i = 0; i < clipText.length(); i++)
- {
- if (clipText[i] == '\n')
- {
- clipTextLines.back().second = i;
- clipTextLines.push_back({ i + 1, 0 });
- }
- }
- clipTextLines.back().second = clipText.length();
- canPasteToMultipleCursors = clipTextLines.size() == mState.mCurrentCursor + 1;
- }
- if (clipText.length() > 0)
- {
- UndoRecord u;
- u.mBefore = mState;
- if (AnyCursorHasSelection())
- {
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- u.mOperations.push_back({ GetSelectedText(c), mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd(), UndoOperationType::Delete });
- DeleteSelection(c);
- }
- }
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- Coordinates start = GetActualCursorCoordinates(c);
- if (canPasteToMultipleCursors)
- {
- std::string clipSubText = clipText.substr(clipTextLines[c].first, clipTextLines[c].second - clipTextLines[c].first);
- InsertTextAtCursor(clipSubText, c);
- u.mOperations.push_back({ clipSubText, start, GetActualCursorCoordinates(c), UndoOperationType::Add });
- }
- else
- {
- InsertTextAtCursor(clipText, c);
- u.mOperations.push_back({ clipText, start, GetActualCursorCoordinates(c), UndoOperationType::Add });
- }
- }
- u.mAfter = mState;
- AddUndo(u);
- }
- }
- 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);
- }
- void TextEditor::SetText(const std::string& aText)
- {
- mLines.clear();
- mLines.emplace_back(Line());
- for (auto chr : aText)
- {
- if (chr == '\r')
- continue;
- if (chr == '\n')
- mLines.emplace_back(Line());
- else
- {
- mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default));
- }
- }
- mScrollToTop = true;
- mUndoBuffer.clear();
- mUndoIndex = 0;
- Colorize();
- }
- std::string TextEditor::GetText(const Coordinates& aStart, const Coordinates& aEnd) const
- {
- assert(aEnd > aStart);
- std::string result;
- auto lstart = aStart.mLine;
- auto lend = aEnd.mLine;
- auto istart = GetCharacterIndexR(aStart);
- auto iend = GetCharacterIndexR(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';
- }
- }
- // Erase the NULL characters that are left because the text editor works with chars internally
- result.erase(std::find(result.begin(), result.end(), '\0'), result.end());
- return result;
- }
- void TextEditor::SetTextLines(const std::vector<std::string>& 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));
- }
- }
- mScrollToTop = true;
- mUndoBuffer.clear();
- mUndoIndex = 0;
- Colorize();
- }
- std::vector<std::string> TextEditor::GetTextLines() const
- {
- std::vector<std::string> 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;
- }
- bool TextEditor::Render(const char* aTitle, bool aParentIsFocused, const ImVec2& aSize, bool aBorder)
- {
- if (mCursorPositionChanged)
- OnCursorPositionChanged();
- mCursorPositionChanged = false;
- 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_NoMove | ImGuiWindowFlags_NoNavInputs);
- bool isFocused = ImGui::IsWindowFocused();
- HandleKeyboardInputs(aParentIsFocused);
- HandleMouseInputs();
- ColorizeInternal();
- Render(aParentIsFocused);
- ImGui::EndChild();
- ImGui::PopStyleVar();
- ImGui::PopStyleColor();
- return isFocused;
- }
- // ------------------------------------ //
- // ---------- Generic utils ----------- //
- // 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(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;
- }
- }
- static inline bool CharIsWordChar(char ch)
- {
- int sizeInBytes = UTF8CharLength(ch);
- return sizeInBytes > 1 ||
- ch >= 'a' && ch <= 'z' ||
- ch >= 'A' && ch <= 'Z' ||
- ch >= '0' && ch <= '9' ||
- ch == '_';
- }
- // ------------------------------------ //
- // ------------- Internal ------------- //
- // ---------- Editor state functions --------- //
- void TextEditor::EditorState::AddCursor()
- {
- // vector is never resized to smaller size, mCurrentCursor points to last available cursor in vector
- mCurrentCursor++;
- mCursors.resize(mCurrentCursor + 1);
- mLastAddedCursor = mCurrentCursor;
- }
- int TextEditor::EditorState::GetLastAddedCursorIndex()
- {
- return mLastAddedCursor > mCurrentCursor ? 0 : mLastAddedCursor;
- }
- void TextEditor::EditorState::SortCursorsFromTopToBottom()
- {
- Coordinates lastAddedCursorPos = mCursors[GetLastAddedCursorIndex()].mInteractiveEnd;
- std::sort(mCursors.begin(), mCursors.begin() + (mCurrentCursor + 1), [](const Cursor& a, const Cursor& b) -> bool
- {
- return a.GetSelectionStart() < b.GetSelectionStart();
- });
- // update last added cursor index to be valid after sort
- for (int c = mCurrentCursor; c > -1; c--)
- if (mCursors[c].mInteractiveEnd == lastAddedCursorPos)
- mLastAddedCursor = c;
- }
- // ---------- Undo record functions --------- //
- TextEditor::UndoRecord::UndoRecord(const std::vector<UndoOperation>& aOperations,
- TextEditor::EditorState& aBefore, TextEditor::EditorState& aAfter)
- {
- mOperations = aOperations;
- mBefore = aBefore;
- mAfter = aAfter;
- for (const UndoOperation& o : mOperations)
- assert(o.mStart <= o.mEnd);
- }
- void TextEditor::UndoRecord::Undo(TextEditor* aEditor)
- {
- for (int i = mOperations.size() - 1; i > -1; i--)
- {
- const UndoOperation& operation = mOperations[i];
- if (!operation.mText.empty())
- {
- switch (operation.mType)
- {
- case UndoOperationType::Delete:
- {
- auto start = operation.mStart;
- aEditor->InsertTextAt(start, operation.mText.c_str());
- aEditor->Colorize(operation.mStart.mLine - 1, operation.mEnd.mLine - operation.mStart.mLine + 2);
- break;
- }
- case UndoOperationType::Add:
- {
- aEditor->DeleteRange(operation.mStart, operation.mEnd);
- aEditor->Colorize(operation.mStart.mLine - 1, operation.mEnd.mLine - operation.mStart.mLine + 2);
- break;
- }
- }
- }
- }
- aEditor->mState = mBefore;
- aEditor->EnsureCursorVisible();
- }
- void TextEditor::UndoRecord::Redo(TextEditor* aEditor)
- {
- for (int i = 0; i < mOperations.size(); i++)
- {
- const UndoOperation& operation = mOperations[i];
- if (!operation.mText.empty())
- {
- switch (operation.mType)
- {
- case UndoOperationType::Delete:
- {
- aEditor->DeleteRange(operation.mStart, operation.mEnd);
- aEditor->Colorize(operation.mStart.mLine - 1, operation.mEnd.mLine - operation.mStart.mLine + 1);
- break;
- }
- case UndoOperationType::Add:
- {
- auto start = operation.mStart;
- aEditor->InsertTextAt(start, operation.mText.c_str());
- aEditor->Colorize(operation.mStart.mLine - 1, operation.mEnd.mLine - operation.mStart.mLine + 1);
- break;
- }
- }
- }
- }
- aEditor->mState = mAfter;
- aEditor->EnsureCursorVisible();
- }
- // ---------- Text editor internal functions --------- //
- std::string TextEditor::GetText() const
- {
- auto lastLine = (int)mLines.size() - 1;
- auto lastLineLength = GetLineMaxColumn(lastLine);
- return GetText(Coordinates(), Coordinates(lastLine, lastLineLength));
- }
- std::string TextEditor::GetClipboardText() const
- {
- std::string result;
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- if (mState.mCursors[c].GetSelectionStart() < mState.mCursors[c].GetSelectionEnd())
- {
- if (result.length() != 0)
- result += '\n';
- result += GetText(mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd());
- }
- }
- return result;
- }
- std::string TextEditor::GetSelectedText(int aCursor) const
- {
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- return GetText(mState.mCursors[aCursor].GetSelectionStart(), mState.mCursors[aCursor].GetSelectionEnd());
- }
- void TextEditor::SetCursorPosition(const Coordinates& aPosition, int aCursor, bool aClearSelection)
- {
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- mCursorPositionChanged = true;
- if (aClearSelection)
- mState.mCursors[aCursor].mInteractiveStart = aPosition;
- if (mState.mCursors[aCursor].mInteractiveEnd != aPosition)
- {
- mState.mCursors[aCursor].mInteractiveEnd = aPosition;
- EnsureCursorVisible();
- }
- }
- int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char* aValue)
- {
- assert(!mReadOnly);
- int cindex = GetCharacterIndexR(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];
- AddGlyphsToLine(aWhere.mLine + 1, 0, line.begin() + cindex, line.end());
- RemoveGlyphsFromLine(aWhere.mLine, cindex);
- }
- 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')
- AddGlyphToLine(aWhere.mLine, cindex++, Glyph(*aValue++, PaletteIndex::Default));
- aWhere.mColumn = GetCharacterColumn(aWhere.mLine, cindex);
- }
- }
- return totalLines;
- }
- void TextEditor::InsertTextAtCursor(const std::string& aValue, int aCursor)
- {
- InsertTextAtCursor(aValue.c_str(), aCursor);
- }
- void TextEditor::InsertTextAtCursor(const char* aValue, int aCursor)
- {
- if (aValue == nullptr)
- return;
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- auto pos = GetActualCursorCoordinates(aCursor);
- auto start = std::min(pos, mState.mCursors[aCursor].GetSelectionStart());
- int totalLines = pos.mLine - start.mLine;
- totalLines += InsertTextAt(pos, aValue);
- SetCursorPosition(pos, aCursor);
- Colorize(start.mLine - 1, totalLines + 2);
- }
- bool TextEditor::Move(int& aLine, int& aCharIndex, bool aLeft, bool aLockLine) const
- {
- // assumes given char index is not in the middle of utf8 sequence
- // char index can be line.length()
- // invalid line
- if (aLine >= mLines.size())
- return false;
- if (aLeft)
- {
- if (aCharIndex == 0)
- {
- if (aLockLine || aLine == 0)
- return false;
- aLine--;
- aCharIndex = mLines[aLine].size();
- }
- else
- {
- aCharIndex--;
- while (aCharIndex > 0 && IsUTFSequence(mLines[aLine][aCharIndex].mChar))
- aCharIndex--;
- }
- }
- else // right
- {
- if (aCharIndex == mLines[aLine].size())
- {
- if (aLockLine || aLine == mLines.size() - 1)
- return false;
- aLine++;
- aCharIndex = 0;
- }
- else
- {
- int seqLength = UTF8CharLength(mLines[aLine][aCharIndex].mChar);
- aCharIndex = std::min(aCharIndex + seqLength, (int)mLines[aLine].size());
- }
- }
- return true;
- }
- void TextEditor::MoveCharIndexAndColumn(int aLine, int& aCharIndex, int& aColumn) const
- {
- assert(aLine < mLines.size());
- assert(aCharIndex < mLines[aLine].size());
- char c = mLines[aLine][aCharIndex].mChar;
- aCharIndex += UTF8CharLength(c);
- if (c == '\t')
- aColumn = (aColumn / mTabSize) * mTabSize + mTabSize;
- else
- aColumn++;
- }
- void TextEditor::MoveCoords(Coordinates& aCoords, MoveDirection aDirection, bool aWordMode, int aLineCount) const
- {
- int charIndex = GetCharacterIndexR(aCoords);
- int lineIndex = aCoords.mLine;
- switch (aDirection)
- {
- case MoveDirection::Right:
- if (charIndex >= mLines[lineIndex].size())
- {
- if (lineIndex < mLines.size() - 1)
- {
- aCoords.mLine = std::max(0, std::min((int)mLines.size() - 1, lineIndex + 1));
- aCoords.mColumn = 0;
- }
- }
- else
- {
- Move(lineIndex, charIndex);
- int oneStepRightColumn = GetCharacterColumn(lineIndex, charIndex);
- if (aWordMode)
- {
- aCoords = FindWordEnd(aCoords);
- aCoords.mColumn = std::max(aCoords.mColumn, oneStepRightColumn);
- }
- else
- aCoords.mColumn = oneStepRightColumn;
- }
- break;
- case MoveDirection::Left:
- if (charIndex == 0)
- {
- if (lineIndex > 0)
- {
- aCoords.mLine = lineIndex - 1;
- aCoords.mColumn = GetLineMaxColumn(aCoords.mLine);
- }
- }
- else
- {
- Move(lineIndex, charIndex, true);
- aCoords.mColumn = GetCharacterColumn(lineIndex, charIndex);
- if (aWordMode)
- aCoords = FindWordStart(aCoords);
- }
- break;
- case MoveDirection::Up:
- aCoords.mLine = std::max(0, lineIndex - aLineCount);
- break;
- case MoveDirection::Down:
- aCoords.mLine = std::max(0, std::min((int)mLines.size() - 1, lineIndex + aLineCount));
- break;
- }
- }
- void TextEditor::MoveUp(int aAmount, bool aSelect)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- Coordinates newCoords = mState.mCursors[c].mInteractiveEnd;
- MoveCoords(newCoords, MoveDirection::Up, false, aAmount);
- SetCursorPosition(newCoords, c, !aSelect);
- }
- EnsureCursorVisible();
- }
- void TextEditor::MoveDown(int aAmount, bool aSelect)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- assert(mState.mCursors[c].mInteractiveEnd.mColumn >= 0);
- Coordinates newCoords = mState.mCursors[c].mInteractiveEnd;
- MoveCoords(newCoords, MoveDirection::Down, false, aAmount);
- SetCursorPosition(newCoords, c, !aSelect);
- }
- EnsureCursorVisible();
- }
- void TextEditor::MoveLeft(bool aSelect, bool aWordMode)
- {
- if (mLines.empty())
- return;
- if (AnyCursorHasSelection() && !aSelect && !aWordMode)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- SetCursorPosition(mState.mCursors[c].GetSelectionStart(), c);
- }
- else
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- Coordinates newCoords = mState.mCursors[c].mInteractiveEnd;
- MoveCoords(newCoords, MoveDirection::Left, aWordMode);
- SetCursorPosition(newCoords, c, !aSelect);
- }
- }
- EnsureCursorVisible();
- }
- void TextEditor::MoveRight(bool aSelect, bool aWordMode)
- {
- if (mLines.empty())
- return;
- if (AnyCursorHasSelection() && !aSelect && !aWordMode)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- SetCursorPosition(mState.mCursors[c].GetSelectionEnd(), c);
- }
- else
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- Coordinates newCoords = mState.mCursors[c].mInteractiveEnd;
- MoveCoords(newCoords, MoveDirection::Right, aWordMode);
- SetCursorPosition(newCoords, c, !aSelect);
- }
- }
- EnsureCursorVisible();
- }
- void TextEditor::MoveTop(bool aSelect)
- {
- SetCursorPosition(Coordinates(0, 0), mState.mCurrentCursor, !aSelect);
- }
- void TextEditor::TextEditor::MoveBottom(bool aSelect)
- {
- int maxLine = (int)mLines.size() - 1;
- Coordinates newPos = Coordinates(maxLine, GetLineMaxColumn(maxLine));
- SetCursorPosition(newPos, mState.mCurrentCursor, !aSelect);
- }
- void TextEditor::MoveHome(bool aSelect)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- SetCursorPosition(Coordinates(mState.mCursors[c].mInteractiveEnd.mLine, 0), c, !aSelect);
- }
- void TextEditor::MoveEnd(bool aSelect)
- {
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- int lindex = mState.mCursors[c].mInteractiveEnd.mLine;
- SetCursorPosition(Coordinates(lindex, GetLineMaxColumn(lindex)), c, !aSelect);
- }
- }
- void TextEditor::EnterCharacter(ImWchar aChar, bool aShift)
- {
- assert(!mReadOnly);
- bool hasSelection = AnyCursorHasSelection();
- bool anyCursorHasMultilineSelection = false;
- for (int c = mState.mCurrentCursor; c > -1; c--)
- if (mState.mCursors[c].GetSelectionStart().mLine != mState.mCursors[c].GetSelectionEnd().mLine)
- {
- anyCursorHasMultilineSelection = true;
- break;
- }
- bool isIndentOperation = hasSelection && anyCursorHasMultilineSelection && aChar == '\t';
- if (isIndentOperation)
- {
- ChangeCurrentLinesIndentation(!aShift);
- return;
- }
- UndoRecord u;
- u.mBefore = mState;
- if (hasSelection)
- {
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- u.mOperations.push_back({ GetSelectedText(c), mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd(), UndoOperationType::Delete });
- DeleteSelection(c);
- }
- }
- std::vector<Coordinates> coords;
- for (int c = mState.mCurrentCursor; c > -1; c--) // order important here for typing \n in the same line at the same time
- {
- auto coord = GetActualCursorCoordinates(c);
- coords.push_back(coord);
- UndoOperation added;
- added.mType = UndoOperationType::Add;
- added.mStart = coord;
- assert(!mLines.empty());
- if (aChar == '\n')
- {
- InsertLine(coord.mLine + 1);
- auto& line = mLines[coord.mLine];
- auto& newLine = mLines[coord.mLine + 1];
- added.mText = "";
- added.mText += (char)aChar;
- if (mAutoIndent)
- for (int i = 0; i < line.size() && isascii(line[i].mChar) && isblank(line[i].mChar); ++i)
- {
- newLine.push_back(line[i]);
- added.mText += line[i].mChar;
- }
- const size_t whitespaceSize = newLine.size();
- auto cindex = GetCharacterIndexR(coord);
- AddGlyphsToLine(coord.mLine + 1, newLine.size(), line.begin() + cindex, line.end());
- RemoveGlyphsFromLine(coord.mLine, cindex);
- SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize)), c);
- }
- else
- {
- char buf[7];
- int e = ImTextCharToUtf8(buf, 7, aChar);
- if (e > 0)
- {
- buf[e] = '\0';
- auto& line = mLines[coord.mLine];
- auto cindex = GetCharacterIndexR(coord);
- if (mOverwrite && cindex < (int)line.size())
- {
- auto d = UTF8CharLength(line[cindex].mChar);
- UndoOperation removed;
- removed.mType = UndoOperationType::Delete;
- removed.mStart = mState.mCursors[c].mInteractiveEnd;
- removed.mEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d));
- while (d-- > 0 && cindex < (int)line.size())
- {
- removed.mText += line[cindex].mChar;
- RemoveGlyphsFromLine(coord.mLine, cindex, cindex + 1);
- }
- u.mOperations.push_back(removed);
- }
- for (auto p = buf; *p != '\0'; p++, ++cindex)
- AddGlyphToLine(coord.mLine, cindex, Glyph(*p, PaletteIndex::Default));
- added.mText = buf;
- SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex)), c);
- }
- else
- continue;
- }
- added.mEnd = GetActualCursorCoordinates(c);
- u.mOperations.push_back(added);
- }
- u.mAfter = mState;
- AddUndo(u);
- for (const auto& coord : coords)
- Colorize(coord.mLine - 1, 3);
- EnsureCursorVisible();
- }
- void TextEditor::Backspace(bool aWordMode)
- {
- assert(!mReadOnly);
- if (mLines.empty())
- return;
- if (AnyCursorHasSelection())
- Delete(aWordMode);
- else
- {
- EditorState stateBeforeDeleting = mState;
- MoveLeft(true, aWordMode);
- if (!AllCursorsHaveSelection()) // can't do backspace if any cursor at {0,0}
- {
- if (AnyCursorHasSelection())
- MoveRight();
- return;
- }
-
- OnCursorPositionChanged(); // might combine cursors
- Delete(aWordMode, &stateBeforeDeleting);
- }
- }
- void TextEditor::Delete(bool aWordMode, const EditorState* aEditorState)
- {
- assert(!mReadOnly);
- if (mLines.empty())
- return;
- if (AnyCursorHasSelection())
- {
- UndoRecord u;
- u.mBefore = aEditorState == nullptr ? mState : *aEditorState;
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- if (!mState.mCursors[c].HasSelection())
- continue;
- u.mOperations.push_back({ GetSelectedText(c), mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd(), UndoOperationType::Delete });
- DeleteSelection(c);
- }
- u.mAfter = mState;
- AddUndo(u);
- }
- else
- {
- EditorState stateBeforeDeleting = mState;
- MoveRight(true, aWordMode);
- if (!AllCursorsHaveSelection()) // can't do delete if any cursor at end of last line
- {
- if (AnyCursorHasSelection())
- MoveLeft();
- return;
- }
- OnCursorPositionChanged(); // might combine cursors
- Delete(aWordMode, &stateBeforeDeleting);
- }
- }
- void TextEditor::SetSelection(Coordinates aStart, Coordinates aEnd, int aCursor)
- {
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- Coordinates minCoords = Coordinates(0, 0);
- int maxLine = (int)mLines.size() - 1;
- Coordinates maxCoords = Coordinates(maxLine, GetLineMaxColumn(maxLine));
- if (aStart < minCoords)
- aStart = minCoords;
- else if (aStart > maxCoords)
- aStart = maxCoords;
- if (aEnd < minCoords)
- aEnd = minCoords;
- else if (aEnd > maxCoords)
- aEnd = maxCoords;
- mState.mCursors[aCursor].mInteractiveStart = aStart;
- SetCursorPosition(aEnd, aCursor, false);
- }
- void TextEditor::SetSelection(int aStartLine, int aStartChar, int aEndLine, int aEndChar, int aCursor)
- {
- Coordinates startCoords = { aStartLine, GetCharacterColumn(aStartLine, aStartChar) };
- Coordinates endCoords = { aEndLine, GetCharacterColumn(aEndLine, aEndChar) };
- SetSelection(startCoords, endCoords, aCursor);
- }
- void TextEditor::SelectNextOccurrenceOf(const char* aText, int aTextSize, int aCursor, bool aCaseSensitive)
- {
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- Coordinates nextStart, nextEnd;
- FindNextOccurrence(aText, aTextSize, mState.mCursors[aCursor].mInteractiveEnd, nextStart, nextEnd, aCaseSensitive);
- SetSelection(nextStart, nextEnd, aCursor);
- EnsureCursorVisible(aCursor, true);
- }
- void TextEditor::AddCursorForNextOccurrence(bool aCaseSensitive)
- {
- const Cursor& currentCursor = mState.mCursors[mState.GetLastAddedCursorIndex()];
- if (currentCursor.GetSelectionStart() == currentCursor.GetSelectionEnd())
- return;
- std::string selectionText = GetText(currentCursor.GetSelectionStart(), currentCursor.GetSelectionEnd());
- Coordinates nextStart, nextEnd;
- if (!FindNextOccurrence(selectionText.c_str(), selectionText.length(), currentCursor.GetSelectionEnd(), nextStart, nextEnd, aCaseSensitive))
- return;
- mState.AddCursor();
- SetSelection(nextStart, nextEnd, mState.mCurrentCursor);
- mState.SortCursorsFromTopToBottom();
- MergeCursorsIfPossible();
- EnsureCursorVisible(-1, true);
- }
- bool TextEditor::FindNextOccurrence(const char* aText, int aTextSize, const Coordinates& aFrom, Coordinates& outStart, Coordinates& outEnd, bool aCaseSensitive)
- {
- assert(aTextSize > 0);
- bool fmatches = false;
- int fline, ifline;
- int findex, ifindex;
- ifline = fline = aFrom.mLine;
- ifindex = findex = GetCharacterIndexR(aFrom);
- while (true)
- {
- bool matches;
- { // match function
- int lineOffset = 0;
- int currentCharIndex = findex;
- int i = 0;
- for (; i < aTextSize; i++)
- {
- if (currentCharIndex == mLines[fline + lineOffset].size())
- {
- if (aText[i] == '\n' && fline + lineOffset + 1 < mLines.size())
- {
- currentCharIndex = 0;
- lineOffset++;
- }
- else
- break;
- }
- else
- {
- char toCompareA = mLines[fline + lineOffset][currentCharIndex].mChar;
- char toCompareB = aText[i];
- toCompareA = (!aCaseSensitive && toCompareA >= 'A' && toCompareA <= 'Z') ? toCompareA - 'A' + 'a' : toCompareA;
- toCompareB = (!aCaseSensitive && toCompareB >= 'A' && toCompareB <= 'Z') ? toCompareB - 'A' + 'a' : toCompareB;
- if (toCompareA != toCompareB)
- break;
- else
- currentCharIndex++;
- }
- }
- matches = i == aTextSize;
- if (matches)
- {
- outStart = { fline, GetCharacterColumn(fline, findex) };
- outEnd = { fline + lineOffset, GetCharacterColumn(fline + lineOffset, currentCharIndex) };
- return true;
- }
- }
- // move forward
- if (findex == mLines[fline].size()) // need to consider line breaks
- {
- if (fline == mLines.size() - 1)
- {
- fline = 0;
- findex = 0;
- }
- else
- {
- fline++;
- findex = 0;
- }
- }
- else
- findex++;
- // detect complete scan
- if (findex == ifindex && fline == ifline)
- return false;
- }
- return false;
- }
- bool TextEditor::FindMatchingBracket(int aLine, int aCharIndex, Coordinates& out)
- {
- if (aLine > mLines.size() - 1)
- return false;
- int maxCharIndex = mLines[aLine].size() - 1;
- if (aCharIndex > maxCharIndex)
- return false;
- int currentLine = aLine;
- int currentCharIndex = aCharIndex;
- int counter = 1;
- if (CLOSE_TO_OPEN_CHAR.find(mLines[aLine][aCharIndex].mChar) != CLOSE_TO_OPEN_CHAR.end())
- {
- char closeChar = mLines[aLine][aCharIndex].mChar;
- char openChar = CLOSE_TO_OPEN_CHAR.at(closeChar);
- while (Move(currentLine, currentCharIndex, true))
- {
- if (currentCharIndex < mLines[currentLine].size())
- {
- char currentChar = mLines[currentLine][currentCharIndex].mChar;
- if (currentChar == openChar)
- {
- counter--;
- if (counter == 0)
- {
- out = { currentLine, GetCharacterColumn(currentLine, currentCharIndex) };
- return true;
- }
- }
- else if (currentChar == closeChar)
- counter++;
- }
- }
- }
- else if (OPEN_TO_CLOSE_CHAR.find(mLines[aLine][aCharIndex].mChar) != OPEN_TO_CLOSE_CHAR.end())
- {
- char openChar = mLines[aLine][aCharIndex].mChar;
- char closeChar = OPEN_TO_CLOSE_CHAR.at(openChar);
- while (Move(currentLine, currentCharIndex))
- {
- if (currentCharIndex < mLines[currentLine].size())
- {
- char currentChar = mLines[currentLine][currentCharIndex].mChar;
- if (currentChar == closeChar)
- {
- counter--;
- if (counter == 0)
- {
- out = { currentLine, GetCharacterColumn(currentLine, currentCharIndex) };
- return true;
- }
- }
- else if (currentChar == openChar)
- counter++;
- }
- }
- }
- return false;
- }
- void TextEditor::ChangeCurrentLinesIndentation(bool aIncrease)
- {
- assert(!mReadOnly);
- UndoRecord u;
- u.mBefore = mState;
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- for (int currentLine = mState.mCursors[c].GetSelectionEnd().mLine; currentLine >= mState.mCursors[c].GetSelectionStart().mLine; currentLine--)
- {
- if (Coordinates{ currentLine, 0 } == mState.mCursors[c].GetSelectionEnd() && mState.mCursors[c].GetSelectionEnd() != mState.mCursors[c].GetSelectionStart()) // when selection ends at line start
- continue;
- if (aIncrease)
- {
- if (mLines[currentLine].size() > 0)
- {
- Coordinates lineStart = { currentLine, 0 };
- Coordinates insertionEnd = lineStart;
- InsertTextAt(insertionEnd, "\t"); // sets insertion end
- u.mOperations.push_back({ "\t", lineStart, insertionEnd, UndoOperationType::Add });
- Colorize(lineStart.mLine, 1);
- }
- }
- else
- {
- Coordinates start = { currentLine, 0 };
- Coordinates end = { currentLine, mTabSize };
- int charIndex = GetCharacterIndexL(end) - 1;
- while (charIndex > -1 && (mLines[currentLine][charIndex].mChar == ' ' || mLines[currentLine][charIndex].mChar == '\t')) charIndex--;
- bool onlySpaceCharactersFound = charIndex == -1;
- if (onlySpaceCharactersFound)
- {
- u.mOperations.push_back({ GetText(start, end), start, end, UndoOperationType::Delete });
- DeleteRange(start, end);
- Colorize(currentLine, 1);
- }
- }
- }
- }
- if (u.mOperations.size() > 0)
- AddUndo(u);
- }
- void TextEditor::MoveUpCurrentLines()
- {
- assert(!mReadOnly);
- UndoRecord u;
- u.mBefore = mState;
- std::set<int> affectedLines;
- int minLine = -1;
- int maxLine = -1;
- for (int c = mState.mCurrentCursor; c > -1; c--) // cursors are expected to be sorted from top to bottom
- {
- for (int currentLine = mState.mCursors[c].GetSelectionEnd().mLine; currentLine >= mState.mCursors[c].GetSelectionStart().mLine; currentLine--)
- {
- if (Coordinates{ currentLine, 0 } == mState.mCursors[c].GetSelectionEnd() && mState.mCursors[c].GetSelectionEnd() != mState.mCursors[c].GetSelectionStart()) // when selection ends at line start
- continue;
- affectedLines.insert(currentLine);
- minLine = minLine == -1 ? currentLine : (currentLine < minLine ? currentLine : minLine);
- maxLine = maxLine == -1 ? currentLine : (currentLine > maxLine ? currentLine : maxLine);
- }
- }
- if (minLine == 0) // can't move up anymore
- return;
- Coordinates start = { minLine - 1, 0 };
- Coordinates end = { maxLine, GetLineMaxColumn(maxLine) };
- u.mOperations.push_back({ GetText(start, end), start, end, UndoOperationType::Delete });
- for (int line : affectedLines) // lines should be sorted here
- std::swap(mLines[line - 1], mLines[line]);
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- mState.mCursors[c].mInteractiveStart.mLine -= 1;
- mState.mCursors[c].mInteractiveEnd.mLine -= 1;
- // no need to set mCursorPositionChanged as cursors will remain sorted
- }
- end = { maxLine, GetLineMaxColumn(maxLine) }; // this line is swapped with line above, need to find new max column
- u.mOperations.push_back({ GetText(start, end), start, end, UndoOperationType::Add });
- u.mAfter = mState;
- AddUndo(u);
- }
- void TextEditor::MoveDownCurrentLines()
- {
- assert(!mReadOnly);
- UndoRecord u;
- u.mBefore = mState;
- std::set<int> affectedLines;
- int minLine = -1;
- int maxLine = -1;
- for (int c = 0; c <= mState.mCurrentCursor; c++) // cursors are expected to be sorted from top to bottom
- {
- for (int currentLine = mState.mCursors[c].GetSelectionEnd().mLine; currentLine >= mState.mCursors[c].GetSelectionStart().mLine; currentLine--)
- {
- if (Coordinates{ currentLine, 0 } == mState.mCursors[c].GetSelectionEnd() && mState.mCursors[c].GetSelectionEnd() != mState.mCursors[c].GetSelectionStart()) // when selection ends at line start
- continue;
- affectedLines.insert(currentLine);
- minLine = minLine == -1 ? currentLine : (currentLine < minLine ? currentLine : minLine);
- maxLine = maxLine == -1 ? currentLine : (currentLine > maxLine ? currentLine : maxLine);
- }
- }
- if (maxLine == mLines.size() - 1) // can't move down anymore
- return;
- Coordinates start = { minLine, 0 };
- Coordinates end = { maxLine + 1, GetLineMaxColumn(maxLine + 1)};
- u.mOperations.push_back({ GetText(start, end), start, end, UndoOperationType::Delete });
- std::set<int>::reverse_iterator rit;
- for (rit = affectedLines.rbegin(); rit != affectedLines.rend(); rit++) // lines should be sorted here
- std::swap(mLines[*rit + 1], mLines[*rit]);
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- mState.mCursors[c].mInteractiveStart.mLine += 1;
- mState.mCursors[c].mInteractiveEnd.mLine += 1;
- // no need to set mCursorPositionChanged as cursors will remain sorted
- }
- end = { maxLine + 1, GetLineMaxColumn(maxLine + 1) }; // this line is swapped with line below, need to find new max column
- u.mOperations.push_back({ GetText(start, end), start, end, UndoOperationType::Add });
- u.mAfter = mState;
- AddUndo(u);
- }
- void TextEditor::ToggleLineComment()
- {
- assert(!mReadOnly);
- if (mLanguageDefinition == nullptr)
- return;
- const std::string& commentString = mLanguageDefinition->mSingleLineComment;
- UndoRecord u;
- u.mBefore = mState;
- bool shouldAddComment = false;
- std::unordered_set<int> affectedLines;
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- for (int currentLine = mState.mCursors[c].GetSelectionEnd().mLine; currentLine >= mState.mCursors[c].GetSelectionStart().mLine; currentLine--)
- {
- if (Coordinates{ currentLine, 0 } == mState.mCursors[c].GetSelectionEnd() && mState.mCursors[c].GetSelectionEnd() != mState.mCursors[c].GetSelectionStart()) // when selection ends at line start
- continue;
- affectedLines.insert(currentLine);
- int currentIndex = 0;
- while (currentIndex < mLines[currentLine].size() && (mLines[currentLine][currentIndex].mChar == ' ' || mLines[currentLine][currentIndex].mChar == '\t')) currentIndex++;
- if (currentIndex == mLines[currentLine].size())
- continue;
- int i = 0;
- while (i < commentString.length() && currentIndex + i < mLines[currentLine].size() && mLines[currentLine][currentIndex + i].mChar == commentString[i]) i++;
- bool matched = i == commentString.length();
- shouldAddComment |= !matched;
- }
- }
- if (shouldAddComment)
- {
- for (int currentLine : affectedLines) // order doesn't matter as changes are not multiline
- {
- Coordinates lineStart = { currentLine, 0 };
- Coordinates insertionEnd = lineStart;
- InsertTextAt(insertionEnd, (commentString + ' ').c_str()); // sets insertion end
- u.mOperations.push_back({ (commentString + ' ') , lineStart, insertionEnd, UndoOperationType::Add });
- Colorize(lineStart.mLine, 1);
- }
- }
- else
- {
- for (int currentLine : affectedLines) // order doesn't matter as changes are not multiline
- {
- int currentIndex = 0;
- while (currentIndex < mLines[currentLine].size() && (mLines[currentLine][currentIndex].mChar == ' ' || mLines[currentLine][currentIndex].mChar == '\t')) currentIndex++;
- if (currentIndex == mLines[currentLine].size())
- continue;
- int i = 0;
- while (i < commentString.length() && currentIndex + i < mLines[currentLine].size() && mLines[currentLine][currentIndex + i].mChar == commentString[i]) i++;
- bool matched = i == commentString.length();
- assert(matched);
- if (currentIndex + i < mLines[currentLine].size() && mLines[currentLine][currentIndex + i].mChar == ' ')
- i++;
- Coordinates start = { currentLine, GetCharacterColumn(currentLine, currentIndex) };
- Coordinates end = { currentLine, GetCharacterColumn(currentLine, currentIndex + i) };
- u.mOperations.push_back({ GetText(start, end) , start, end, UndoOperationType::Delete});
- DeleteRange(start, end);
- Colorize(currentLine, 1);
- }
- }
- u.mAfter = mState;
- AddUndo(u);
- }
- void TextEditor::RemoveCurrentLines()
- {
- UndoRecord u;
- u.mBefore = mState;
- if (AnyCursorHasSelection())
- {
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- if (!mState.mCursors[c].HasSelection())
- continue;
- u.mOperations.push_back({ GetSelectedText(c), mState.mCursors[c].GetSelectionStart(), mState.mCursors[c].GetSelectionEnd(), UndoOperationType::Delete });
- DeleteSelection(c);
- }
- }
- MoveHome();
- OnCursorPositionChanged(); // might combine cursors
- for (int c = mState.mCurrentCursor; c > -1; c--)
- {
- int currentLine = mState.mCursors[c].mInteractiveEnd.mLine;
- int nextLine = currentLine + 1;
- int prevLine = currentLine - 1;
- Coordinates toDeleteStart, toDeleteEnd;
- if (mLines.size() > nextLine) // next line exists
- {
- toDeleteStart = Coordinates(currentLine, 0);
- toDeleteEnd = Coordinates(nextLine, 0);
- SetCursorPosition({ mState.mCursors[c].mInteractiveEnd.mLine, 0 }, c);
- }
- else if (prevLine > -1) // previous line exists
- {
- toDeleteStart = Coordinates(prevLine, GetLineMaxColumn(prevLine));
- toDeleteEnd = Coordinates(currentLine, GetLineMaxColumn(currentLine));
- SetCursorPosition({ prevLine, 0 }, c);
- }
- else
- {
- toDeleteStart = Coordinates(currentLine, 0);
- toDeleteEnd = Coordinates(currentLine, GetLineMaxColumn(currentLine));
- SetCursorPosition({ currentLine, 0 }, c);
- }
- u.mOperations.push_back({ GetText(toDeleteStart, toDeleteEnd), toDeleteStart, toDeleteEnd, UndoOperationType::Delete });
- std::unordered_set<int> handledCursors = { c };
- if (toDeleteStart.mLine != toDeleteEnd.mLine)
- RemoveLine(currentLine, &handledCursors);
- else
- DeleteRange(toDeleteStart, toDeleteEnd);
- }
- u.mAfter = mState;
- AddUndo(u);
- }
- float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom, bool aSanitizeCoords) const
- {
- if (aSanitizeCoords)
- return SanitizeCoordinates(aFrom).mColumn * mCharAdvance.x;
- else
- return aFrom.mColumn * mCharAdvance.x;
- }
- void TextEditor::EnsureCursorVisible(int aCursor, bool aStartToo)
- {
- if (aCursor == -1)
- aCursor = mState.GetLastAddedCursorIndex();
- mEnsureCursorVisible = aCursor;
- mEnsureCursorVisibleStartToo = aStartToo;
- return;
- }
- 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 : GetLineMaxColumn(line, column);
- return Coordinates(line, column);
- }
- }
- TextEditor::Coordinates TextEditor::GetActualCursorCoordinates(int aCursor, bool aStart) const
- {
- if (aCursor == -1)
- return SanitizeCoordinates(aStart ? mState.mCursors[mState.mCurrentCursor].mInteractiveStart : mState.mCursors[mState.mCurrentCursor].mInteractiveEnd);
- else
- return SanitizeCoordinates(aStart ? mState.mCursors[aCursor].mInteractiveStart : mState.mCursors[aCursor].mInteractiveEnd);
- }
- TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode, bool* isOverLineNumber) const
- {
- ImVec2 origin = ImGui::GetCursorScreenPos();
- ImVec2 local(aPosition.x - origin.x + 3.0f, aPosition.y - origin.y);
- if (isOverLineNumber != nullptr)
- *isOverLineNumber = local.x < mTextStart;
- Coordinates out = {
- Max(0, (int)floor(local.y / mCharAdvance.y)),
- Max(0, (int)floor((local.x - mTextStart) / mCharAdvance.x))
- };
- int charIndex = GetCharacterIndexL(out);
- if (charIndex > -1 && charIndex < mLines[out.mLine].size() && mLines[out.mLine][charIndex].mChar == '\t')
- {
- int columnToLeft = GetCharacterColumn(out.mLine, charIndex);
- int columnToRight = GetCharacterColumn(out.mLine, GetCharacterIndexR(out));
- if (out.mColumn - columnToLeft < columnToRight - out.mColumn)
- out.mColumn = columnToLeft;
- else
- out.mColumn = columnToRight;
- }
- else
- out.mColumn = Max(0, (int)floor((local.x - mTextStart + POS_TO_COORDS_COLUMN_OFFSET * mCharAdvance.x) / mCharAdvance.x));
- return SanitizeCoordinates(out);
- }
- TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates& aFrom) const
- {
- if (aFrom.mLine >= (int)mLines.size())
- return aFrom;
- int lineIndex = aFrom.mLine;
- auto& line = mLines[lineIndex];
- int charIndex = GetCharacterIndexL(aFrom);
- if (charIndex > (int)line.size() || line.size() == 0)
- return aFrom;
- if (charIndex == (int)line.size())
- charIndex--;
- bool initialIsWordChar = CharIsWordChar(line[charIndex].mChar);
- bool initialIsSpace = isspace(line[charIndex].mChar);
- char initialChar = line[charIndex].mChar;
- while (Move(lineIndex, charIndex, true, true))
- {
- bool isWordChar = CharIsWordChar(line[charIndex].mChar);
- bool isSpace = isspace(line[charIndex].mChar);
- if (initialIsSpace && !isSpace ||
- initialIsWordChar && !isWordChar ||
- !initialIsWordChar && !initialIsSpace && initialChar != line[charIndex].mChar)
- {
- Move(lineIndex, charIndex, false, true); // one step to the right
- break;
- }
- }
- return { aFrom.mLine, GetCharacterColumn(aFrom.mLine, charIndex) };
- }
- TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates& aFrom) const
- {
- if (aFrom.mLine >= (int)mLines.size())
- return aFrom;
- int lineIndex = aFrom.mLine;
- auto& line = mLines[lineIndex];
- auto charIndex = GetCharacterIndexL(aFrom);
- if (charIndex >= (int)line.size())
- return aFrom;
- bool initialIsWordChar = CharIsWordChar(line[charIndex].mChar);
- bool initialIsSpace = isspace(line[charIndex].mChar);
- char initialChar = line[charIndex].mChar;
- while (Move(lineIndex, charIndex, false, true))
- {
- if (charIndex == line.size())
- break;
- bool isWordChar = CharIsWordChar(line[charIndex].mChar);
- bool isSpace = isspace(line[charIndex].mChar);
- if (initialIsSpace && !isSpace ||
- initialIsWordChar && !isWordChar ||
- !initialIsWordChar && !initialIsSpace && initialChar != line[charIndex].mChar)
- break;
- }
- return { lineIndex, GetCharacterColumn(aFrom.mLine, charIndex) };
- }
- int TextEditor::GetCharacterIndexL(const Coordinates& aCoords) const
- {
- if (aCoords.mLine >= mLines.size())
- return -1;
- auto& line = mLines[aCoords.mLine];
- int c = 0;
- int i = 0;
- int tabCoordsLeft = 0;
- for (; i < line.size() && c < aCoords.mColumn;)
- {
- if (line[i].mChar == '\t')
- {
- if (tabCoordsLeft == 0)
- tabCoordsLeft = TabSizeAtColumn(c);
- if (tabCoordsLeft > 0)
- tabCoordsLeft--;
- c++;
- }
- else
- ++c;
- if (tabCoordsLeft == 0)
- i += UTF8CharLength(line[i].mChar);
- }
- return i;
- }
- int TextEditor::GetCharacterIndexR(const Coordinates& aCoords) const
- {
- if (aCoords.mLine >= mLines.size())
- return -1;
- int c = 0;
- int i = 0;
- for (; i < mLines[aCoords.mLine].size() && c < aCoords.mColumn;)
- MoveCharIndexAndColumn(aCoords.mLine, i, c);
- return i;
- }
- int TextEditor::GetCharacterColumn(int aLine, int aIndex) const
- {
- if (aLine >= mLines.size())
- return 0;
- int c = 0;
- int i = 0;
- while (i < aIndex && i < mLines[aLine].size())
- MoveCharIndexAndColumn(aLine, i, c);
- return c;
- }
- int TextEditor::GetFirstVisibleCharacterIndex(int aLine) const
- {
- if (aLine >= mLines.size())
- return 0;
- int c = 0;
- int i = 0;
- while (c < mFirstVisibleColumn && i < mLines[aLine].size())
- MoveCharIndexAndColumn(aLine, i, c);
- if (c > mFirstVisibleColumn)
- i--;
- return i;
- }
- int TextEditor::GetLineMaxColumn(int aLine, int aLimit) const
- {
- if (aLine >= mLines.size())
- return 0;
- int c = 0;
- if (aLimit == -1)
- {
- for (int i = 0; i < mLines[aLine].size(); )
- MoveCharIndexAndColumn(aLine, i, c);
- }
- else
- {
- for (int i = 0; i < mLines[aLine].size(); )
- {
- MoveCharIndexAndColumn(aLine, i, c);
- if (c > aLimit)
- return aLimit;
- }
- }
- return c;
- }
- TextEditor::Line& TextEditor::InsertLine(int aIndex)
- {
- assert(!mReadOnly);
- auto& result = *mLines.insert(mLines.begin() + aIndex, Line());
- for (int c = 0; c <= mState.mCurrentCursor; c++) // handle multiple cursors
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine >= aIndex)
- SetCursorPosition({ mState.mCursors[c].mInteractiveEnd.mLine + 1, mState.mCursors[c].mInteractiveEnd.mColumn }, c);
- }
- return result;
- }
- void TextEditor::RemoveLine(int aIndex, const std::unordered_set<int>* aHandledCursors)
- {
- assert(!mReadOnly);
- assert(mLines.size() > 1);
- mLines.erase(mLines.begin() + aIndex);
- assert(!mLines.empty());
- // handle multiple cursors
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine >= aIndex)
- {
- if (aHandledCursors == nullptr || aHandledCursors->find(c) == aHandledCursors->end()) // move up if has not been handled already
- SetCursorPosition({ mState.mCursors[c].mInteractiveEnd.mLine - 1, mState.mCursors[c].mInteractiveEnd.mColumn }, c);
- }
- }
- }
- void TextEditor::RemoveLines(int aStart, int aEnd)
- {
- assert(!mReadOnly);
- assert(aEnd >= aStart);
- assert(mLines.size() > (size_t)(aEnd - aStart));
- mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd);
- assert(!mLines.empty());
- // handle multiple cursors
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine >= aStart)
- {
- int targetLine = mState.mCursors[c].mInteractiveEnd.mLine - (aEnd - aStart);
- targetLine = targetLine < 0 ? 0 : targetLine;
- SetCursorPosition({ targetLine , mState.mCursors[c].mInteractiveEnd.mColumn }, c);
- }
- }
- }
- void TextEditor::DeleteRange(const Coordinates& aStart, const Coordinates& aEnd)
- {
- assert(aEnd >= aStart);
- assert(!mReadOnly);
- if (aEnd == aStart)
- return;
- auto start = GetCharacterIndexL(aStart);
- auto end = GetCharacterIndexR(aEnd);
- if (aStart.mLine == aEnd.mLine)
- {
- auto n = GetLineMaxColumn(aStart.mLine);
- if (aEnd.mColumn >= n)
- RemoveGlyphsFromLine(aStart.mLine, start); // from start to end of line
- else
- RemoveGlyphsFromLine(aStart.mLine, start, end);
- }
- else
- {
- RemoveGlyphsFromLine(aStart.mLine, start); // from start to end of line
- RemoveGlyphsFromLine(aEnd.mLine, 0, end);
- auto& firstLine = mLines[aStart.mLine];
- auto& lastLine = mLines[aEnd.mLine];
- if (aStart.mLine < aEnd.mLine)
- {
- AddGlyphsToLine(aStart.mLine, firstLine.size(), lastLine.begin(), lastLine.end());
- for (int c = 0; c <= mState.mCurrentCursor; c++) // move up cursors in line that is being moved up
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine > aEnd.mLine)
- break;
- else if (mState.mCursors[c].mInteractiveEnd.mLine != aEnd.mLine)
- continue;
- int otherCursorEndCharIndex = GetCharacterIndexR(mState.mCursors[c].mInteractiveEnd);
- int otherCursorStartCharIndex = GetCharacterIndexR(mState.mCursors[c].mInteractiveStart);
- int otherCursorNewEndCharIndex = GetCharacterIndexR(aStart) + otherCursorEndCharIndex;
- int otherCursorNewStartCharIndex = GetCharacterIndexR(aStart) + otherCursorStartCharIndex;
- auto targetEndCoords = Coordinates(aStart.mLine, GetCharacterColumn(aStart.mLine, otherCursorNewEndCharIndex));
- auto targetStartCoords = Coordinates(aStart.mLine, GetCharacterColumn(aStart.mLine, otherCursorNewStartCharIndex));
- SetCursorPosition(targetStartCoords, c, true);
- SetCursorPosition(targetEndCoords, c, false);
- }
- RemoveLines(aStart.mLine + 1, aEnd.mLine + 1);
- }
- }
- }
- void TextEditor::DeleteSelection(int aCursor)
- {
- if (aCursor == -1)
- aCursor = mState.mCurrentCursor;
- if (mState.mCursors[aCursor].GetSelectionEnd() == mState.mCursors[aCursor].GetSelectionStart())
- return;
- DeleteRange(mState.mCursors[aCursor].GetSelectionStart(), mState.mCursors[aCursor].GetSelectionEnd());
- SetCursorPosition(mState.mCursors[aCursor].GetSelectionStart(), aCursor);
- Colorize(mState.mCursors[aCursor].GetSelectionStart().mLine, 1);
- }
- void TextEditor::RemoveGlyphsFromLine(int aLine, int aStartChar, int aEndChar)
- {
- int column = GetCharacterColumn(aLine, aStartChar);
- auto& line = mLines[aLine];
- OnLineChanged(true, aLine, column, aEndChar - aStartChar, true);
- line.erase(line.begin() + aStartChar, aEndChar == -1 ? line.end() : line.begin() + aEndChar);
- OnLineChanged(false, aLine, column, aEndChar - aStartChar, true);
- }
- void TextEditor::AddGlyphsToLine(int aLine, int aTargetIndex, Line::iterator aSourceStart, Line::iterator aSourceEnd)
- {
- int targetColumn = GetCharacterColumn(aLine, aTargetIndex);
- int charsInserted = std::distance(aSourceStart, aSourceEnd);
- auto& line = mLines[aLine];
- OnLineChanged(true, aLine, targetColumn, charsInserted, false);
- line.insert(line.begin() + aTargetIndex, aSourceStart, aSourceEnd);
- OnLineChanged(false, aLine, targetColumn, charsInserted, false);
- }
- void TextEditor::AddGlyphToLine(int aLine, int aTargetIndex, Glyph aGlyph)
- {
- int targetColumn = GetCharacterColumn(aLine, aTargetIndex);
- auto& line = mLines[aLine];
- OnLineChanged(true, aLine, targetColumn, 1, false);
- line.insert(line.begin() + aTargetIndex, aGlyph);
- OnLineChanged(false, aLine, targetColumn, 1, false);
- }
- ImU32 TextEditor::GetGlyphColor(const Glyph& aGlyph) const
- {
- if (mLanguageDefinition == nullptr)
- 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(bool aParentIsFocused)
- {
- if (ImGui::IsWindowFocused() || aParentIsFocused)
- {
- if (ImGui::IsWindowHovered())
- ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
- //ImGui::CaptureKeyboardFromApp(true);
- ImGuiIO& io = ImGui::GetIO();
- auto isOSX = io.ConfigMacOSXBehaviors;
- auto alt = io.KeyAlt;
- auto ctrl = io.KeyCtrl;
- auto shift = io.KeyShift;
- auto super = io.KeySuper;
- auto isShortcut = (isOSX ? (super && !ctrl) : (ctrl && !super)) && !alt && !shift;
- auto isShiftShortcut = (isOSX ? (super && !ctrl) : (ctrl && !super)) && shift && !alt;
- auto isWordmoveKey = isOSX ? alt : ctrl;
- auto isAltOnly = alt && !ctrl && !shift && !super;
- auto isCtrlOnly = ctrl && !alt && !shift && !super;
- auto isShiftOnly = shift && !alt && !ctrl && !super;
- io.WantCaptureKeyboard = true;
- io.WantTextInput = true;
- if (!mReadOnly && isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z)))
- Undo();
- else if (!mReadOnly && isAltOnly && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)))
- Undo();
- else if (!mReadOnly && isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Y)))
- Redo();
- else if (!mReadOnly && isShiftShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Z)))
- Redo();
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)))
- MoveUp(1, shift);
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)))
- MoveDown(1, shift);
- else if ((isOSX ? !ctrl : !alt) && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)))
- MoveLeft(shift, isWordmoveKey);
- else if ((isOSX ? !ctrl : !alt) && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)))
- MoveRight(shift, isWordmoveKey);
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageUp)))
- MoveUp(mVisibleLineCount - 2, shift);
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_PageDown)))
- MoveDown(mVisibleLineCount - 2, shift);
- else if (ctrl && !alt && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home)))
- MoveTop(shift);
- else if (ctrl && !alt && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End)))
- MoveBottom(shift);
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Home)))
- MoveHome(shift);
- else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_End)))
- MoveEnd(shift);
- else if (!mReadOnly && !alt && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)))
- Delete(ctrl);
- else if (!mReadOnly && !alt && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)))
- Backspace(ctrl);
- else if (!mReadOnly && !alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_K)))
- RemoveCurrentLines();
- else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftBracket)))
- ChangeCurrentLinesIndentation(false);
- else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightBracket)))
- ChangeCurrentLinesIndentation(true);
- else if (!alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)))
- MoveUpCurrentLines();
- else if (!alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)))
- MoveDownCurrentLines();
- else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Slash)))
- ToggleLineComment();
- else if (!alt && !ctrl && !shift && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
- mOverwrite ^= true;
- else if (isCtrlOnly && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
- Copy();
- else if (isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C)))
- Copy();
- else if (!mReadOnly && isShiftOnly && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Insert)))
- Paste();
- else if (!mReadOnly && isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_V)))
- Paste();
- else if (isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_X)))
- Cut();
- else if (isShiftOnly && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete)))
- Cut();
- else if (isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_A)))
- SelectAll();
- else if (isShortcut && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_D)))
- AddCursorForNextOccurrence();
- else if (!mReadOnly && !alt && !ctrl && !shift && !super && (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))))
- EnterCharacter('\n', false);
- else if (!mReadOnly && !alt && !ctrl && !super && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab)))
- EnterCharacter('\t', shift);
- if (!mReadOnly && !io.InputQueueCharacters.empty() && ctrl == alt && !super)
- {
- 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;
- /*
- Pan with middle mouse button
- */
- mPanning &= ImGui::IsMouseDown(2);
- if (mPanning && ImGui::IsMouseDragging(2))
- {
- ImVec2 scroll = { ImGui::GetScrollX(), ImGui::GetScrollY() };
- ImVec2 currentMousePos = ImGui::GetMouseDragDelta(2);
- ImVec2 mouseDelta = {
- currentMousePos.x - mLastMousePos.x,
- currentMousePos.y - mLastMousePos.y
- };
- ImGui::SetScrollY(scroll.y - mouseDelta.y);
- ImGui::SetScrollX(scroll.x - mouseDelta.x);
- mLastMousePos = currentMousePos;
- }
- // Mouse left button dragging (=> update selection)
- mDraggingSelection &= ImGui::IsMouseDown(0);
- if (mDraggingSelection && ImGui::IsMouseDragging(0))
- {
- io.WantCaptureMouse = true;
- Coordinates cursorCoords = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite);
- SetCursorPosition(cursorCoords, mState.GetLastAddedCursorIndex(), false);
- }
- if (ImGui::IsWindowHovered())
- {
- auto click = ImGui::IsMouseClicked(0);
- if (!shift && !alt)
- {
- auto doubleClick = ImGui::IsMouseDoubleClicked(0);
- auto t = ImGui::GetTime();
- auto tripleClick = click && !doubleClick &&
- (mLastClickTime != -1.0f && (t - mLastClickTime) < io.MouseDoubleClickTime &&
- Distance(io.MousePos, mLastClickPos) < 0.01f);
- if (click)
- mDraggingSelection = true;
- /*
- Pan with middle mouse button
- */
- if (ImGui::IsMouseClicked(2))
- {
- mPanning = true;
- mLastMousePos = ImGui::GetMouseDragDelta(2);
- }
- /*
- Left mouse button triple click
- */
- if (tripleClick)
- {
- if (ctrl)
- mState.AddCursor();
- else
- mState.mCurrentCursor = 0;
- Coordinates cursorCoords = ScreenPosToCoordinates(ImGui::GetMousePos());
- Coordinates targetCursorPos = cursorCoords.mLine < mLines.size() - 1 ?
- Coordinates{ cursorCoords.mLine + 1, 0 } :
- Coordinates{ cursorCoords.mLine, GetLineMaxColumn(cursorCoords.mLine) };
- SetSelection({ cursorCoords.mLine, 0 }, targetCursorPos, mState.mCurrentCursor);
- mLastClickTime = -1.0f;
- }
- /*
- Left mouse button double click
- */
- else if (doubleClick)
- {
- if (ctrl)
- mState.AddCursor();
- else
- mState.mCurrentCursor = 0;
- Coordinates cursorCoords = ScreenPosToCoordinates(ImGui::GetMousePos());
- SetSelection(FindWordStart(cursorCoords), FindWordEnd(cursorCoords), mState.mCurrentCursor);
- mLastClickTime = (float)ImGui::GetTime();
- mLastClickPos = io.MousePos;
- }
- /*
- Left mouse button click
- */
- else if (click)
- {
- if (ctrl)
- mState.AddCursor();
- else
- mState.mCurrentCursor = 0;
- bool isOverLineNumber;
- Coordinates cursorCoords = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite, &isOverLineNumber);
- if (isOverLineNumber)
- {
- Coordinates targetCursorPos = cursorCoords.mLine < mLines.size() - 1 ?
- Coordinates{ cursorCoords.mLine + 1, 0 } :
- Coordinates{ cursorCoords.mLine, GetLineMaxColumn(cursorCoords.mLine) };
- SetSelection({ cursorCoords.mLine, 0 }, targetCursorPos, mState.mCurrentCursor);
- }
- else
- SetCursorPosition(cursorCoords, mState.GetLastAddedCursorIndex());
- mLastClickTime = (float)ImGui::GetTime();
- mLastClickPos = io.MousePos;
- }
- else if (ImGui::IsMouseReleased(0))
- {
- mState.SortCursorsFromTopToBottom();
- MergeCursorsIfPossible();
- }
- }
- else if (shift)
- {
- if (click)
- {
- Coordinates newSelection = ScreenPosToCoordinates(ImGui::GetMousePos(), !mOverwrite);
- SetCursorPosition(newSelection, mState.mCurrentCursor, false);
- }
- }
- }
- }
- void TextEditor::UpdateViewVariables(float aScrollX, float aScrollY)
- {
- mContentHeight = ImGui::GetWindowHeight() - (IsHorizontalScrollbarVisible() ? IMGUI_SCROLLBAR_WIDTH : 0.0f);
- mContentWidth = ImGui::GetWindowWidth() - (IsVerticalScrollbarVisible() ? IMGUI_SCROLLBAR_WIDTH : 0.0f);
- mVisibleLineCount = Max((int)ceil(mContentHeight / mCharAdvance.y), 0);
- mFirstVisibleLine = Max((int)(aScrollY / mCharAdvance.y), 0);
- mLastVisibleLine = Max((int)((mContentHeight + aScrollY) / mCharAdvance.y), 0);
- mVisibleColumnCount = Max((int)ceil((mContentWidth - Max(mTextStart - aScrollX, 0.0f)) / mCharAdvance.x), 0);
- mFirstVisibleColumn = Max((int)(Max(aScrollX - mTextStart, 0.0f) / mCharAdvance.x), 0);
- mLastVisibleColumn = Max((int)((mContentWidth + aScrollX - mTextStart) / mCharAdvance.x), 0);
- }
- void TextEditor::Render(bool aParentIsFocused)
- {
- /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/
- const float fontWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x;
- const float fontHeight = ImGui::GetTextLineHeightWithSpacing();
- mCharAdvance = ImVec2(fontWidth, fontHeight * mLineSpacing);
- // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width
- mTextStart = mLeftMargin;
- static char lineNumberBuffer[16];
- if (mShowLineNumbers)
- {
- snprintf(lineNumberBuffer, 16, " %d ", mLines.size());
- mTextStart += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, lineNumberBuffer, nullptr, nullptr).x;
- }
- ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos();
- mScrollX = ImGui::GetScrollX();
- mScrollY = ImGui::GetScrollY();
- UpdateViewVariables(mScrollX, mScrollY);
- int maxColumnLimited = 0;
- if (!mLines.empty())
- {
- auto drawList = ImGui::GetWindowDrawList();
- float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x;
- for (int lineNo = mFirstVisibleLine; lineNo <= mLastVisibleLine && lineNo < mLines.size(); lineNo++)
- {
- ImVec2 lineStartScreenPos = ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y);
- ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y);
- auto& line = mLines[lineNo];
- maxColumnLimited = Max(GetLineMaxColumn(lineNo, mLastVisibleColumn), maxColumnLimited);
- Coordinates lineStartCoord(lineNo, 0);
- Coordinates lineEndCoord(lineNo, maxColumnLimited);
- // Draw selection for the current line
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- float rectStart = -1.0f;
- float rectEnd = -1.0f;
- Coordinates cursorSelectionStart = mState.mCursors[c].GetSelectionStart();
- Coordinates cursorSelectionEnd = mState.mCursors[c].GetSelectionEnd();
- assert(cursorSelectionStart <= cursorSelectionEnd);
- if (cursorSelectionStart <= lineEndCoord)
- rectStart = cursorSelectionStart > lineStartCoord ? TextDistanceToLineStart(cursorSelectionStart) : 0.0f;
- if (cursorSelectionEnd > lineStartCoord)
- rectEnd = TextDistanceToLineStart(cursorSelectionEnd < lineEndCoord ? cursorSelectionEnd : lineEndCoord);
- if (cursorSelectionEnd.mLine > lineNo || cursorSelectionEnd.mLine == lineNo && cursorSelectionEnd > lineEndCoord)
- rectEnd += mCharAdvance.x;
- if (rectStart != -1 && rectEnd != -1 && rectStart < rectEnd)
- drawList->AddRectFilled(
- ImVec2{ lineStartScreenPos.x + mTextStart + rectStart, lineStartScreenPos.y },
- ImVec2{ lineStartScreenPos.x + mTextStart + rectEnd, lineStartScreenPos.y + mCharAdvance.y },
- mPalette[(int)PaletteIndex::Selection]);
- }
- // Draw line number (right aligned)
- if (mShowLineNumbers)
- {
- snprintf(lineNumberBuffer, 16, "%d ", lineNo + 1);
- float lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, lineNumberBuffer, nullptr, nullptr).x;
- drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], lineNumberBuffer);
- }
- std::vector<Coordinates> cursorCoordsInThisLine;
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine == lineNo)
- cursorCoordsInThisLine.push_back(mState.mCursors[c].mInteractiveEnd);
- }
- if (cursorCoordsInThisLine.size() > 0)
- {
- bool focused = ImGui::IsWindowFocused() || aParentIsFocused;
- // Render the cursors
- if (focused)
- {
- for (const auto& cursorCoords : cursorCoordsInThisLine)
- {
- float width = 1.0f;
- auto cindex = GetCharacterIndexR(cursorCoords);
- float cx = TextDistanceToLineStart(cursorCoords);
- if (mOverwrite && cindex < (int)line.size())
- {
- if (line[cindex].mChar == '\t')
- {
- auto x = (1.0f + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize);
- width = x - cx;
- }
- else
- width = mCharAdvance.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 (mCursorOnBracket)
- {
- ImVec2 topLeft = { cstart.x, lineStartScreenPos.y + fontHeight + 1.0f };
- ImVec2 bottomRight = { topLeft.x + mCharAdvance.x, topLeft.y + 1.0f };
- drawList->AddRectFilled(topLeft, bottomRight, mPalette[(int)PaletteIndex::Cursor]);
- }
- }
- }
- }
- // Render colorized text
- static std::string glyphBuffer;
- int charIndex = GetFirstVisibleCharacterIndex(lineNo);
- int column = mFirstVisibleColumn; // can be in the middle of tab character
- while (charIndex < mLines[lineNo].size() && column <= mLastVisibleColumn)
- {
- auto& glyph = line[charIndex];
- auto color = GetGlyphColor(glyph);
- ImVec2 targetGlyphPos = { lineStartScreenPos.x + mTextStart + TextDistanceToLineStart({lineNo, column}, false), lineStartScreenPos.y };
- if (glyph.mChar == '\t')
- {
- if (mShowWhitespaces)
- {
- ImVec2 p1, p2, p3, p4;
- const auto s = ImGui::GetFontSize();
- const auto x1 = targetGlyphPos.x + mCharAdvance.x * 0.3f;
- const auto y = targetGlyphPos.y + fontHeight * 0.5f;
- if (mShortTabs)
- {
- const auto x2 = targetGlyphPos.x + mCharAdvance.x;
- p1 = ImVec2(x1, y);
- p2 = ImVec2(x2, y);
- p3 = ImVec2(x2 - s * 0.16f, y - s * 0.16f);
- p4 = ImVec2(x2 - s * 0.16f, y + s * 0.16f);
- }
- else
- {
- const auto x2 = targetGlyphPos.x + TabSizeAtColumn(column) * mCharAdvance.x - mCharAdvance.x * 0.3f;
- p1 = ImVec2(x1, y);
- p2 = ImVec2(x2, y);
- p3 = ImVec2(x2 - s * 0.2f, y - s * 0.2f);
- p4 = ImVec2(x2 - s * 0.2f, y + s * 0.2f);
- }
- drawList->AddLine(p1, p2, mPalette[(int)PaletteIndex::ControlCharacter]);
- drawList->AddLine(p2, p3, mPalette[(int)PaletteIndex::ControlCharacter]);
- drawList->AddLine(p2, p4, mPalette[(int)PaletteIndex::ControlCharacter]);
- }
- }
- else if (glyph.mChar == ' ')
- {
- if (mShowWhitespaces)
- {
- const auto s = ImGui::GetFontSize();
- const auto x = targetGlyphPos.x + spaceSize * 0.5f;
- const auto y = targetGlyphPos.y + s * 0.5f;
- drawList->AddCircleFilled(ImVec2(x, y), 1.5f, mPalette[(int)PaletteIndex::ControlCharacter], 4);
- }
- }
- else
- {
- int seqLength = UTF8CharLength(glyph.mChar);
- if (mCursorOnBracket && seqLength == 1 && mMatchingBracketCoords == Coordinates{ lineNo, column })
- {
- ImVec2 topLeft = { targetGlyphPos.x, targetGlyphPos.y + fontHeight + 1.0f };
- ImVec2 bottomRight = { topLeft.x + mCharAdvance.x, topLeft.y + 1.0f };
- drawList->AddRectFilled(topLeft, bottomRight, mPalette[(int)PaletteIndex::Cursor]);
- }
- glyphBuffer.clear();
- for (int i = 0; i < seqLength; i++)
- glyphBuffer.push_back(line[charIndex + i].mChar);
- drawList->AddText(targetGlyphPos, color, glyphBuffer.c_str());
- }
- MoveCharIndexAndColumn(lineNo, charIndex, column);
- }
- }
- }
- mCurrentSpaceHeight = (mLines.size() + Min(mVisibleLineCount - 1, (int)mLines.size())) * mCharAdvance.y;
- mCurrentSpaceWidth = Max((maxColumnLimited + Min(mVisibleColumnCount - 1, maxColumnLimited)) * mCharAdvance.x, mCurrentSpaceWidth);
- ImGui::SetCursorPos(ImVec2(0, 0));
- ImGui::Dummy(ImVec2(mCurrentSpaceWidth, mCurrentSpaceHeight));
- if (mEnsureCursorVisible > -1)
- {
- for (int i = 0; i < (mEnsureCursorVisibleStartToo ? 2 : 1); i++) // first pass for interactive end and second pass for interactive start
- {
- if (i) UpdateViewVariables(mScrollX, mScrollY); // second pass depends on changes made in first pass
- Coordinates targetCoords = GetActualCursorCoordinates(mEnsureCursorVisible, i); // cursor selection end or start
- if (targetCoords.mLine <= mFirstVisibleLine)
- {
- float targetScroll = std::max(0.0f, (targetCoords.mLine - 0.5f) * mCharAdvance.y);
- if (targetScroll < mScrollY)
- ImGui::SetScrollY(targetScroll);
- }
- if (targetCoords.mLine >= mLastVisibleLine)
- {
- float targetScroll = std::max(0.0f, (targetCoords.mLine + 1.5f) * mCharAdvance.y - mContentHeight);
- if (targetScroll > mScrollY)
- ImGui::SetScrollY(targetScroll);
- }
- if (targetCoords.mColumn <= mFirstVisibleColumn)
- {
- float targetScroll = std::max(0.0f, mTextStart + (targetCoords.mColumn - 0.5f) * mCharAdvance.x);
- if (targetScroll < mScrollX)
- ImGui::SetScrollX(mScrollX = targetScroll);
- }
- if (targetCoords.mColumn >= mLastVisibleColumn)
- {
- float targetScroll = std::max(0.0f, mTextStart + (targetCoords.mColumn + 0.5f) * mCharAdvance.x - mContentWidth);
- if (targetScroll > mScrollX)
- ImGui::SetScrollX(mScrollX = targetScroll);
- }
- }
- mEnsureCursorVisible = -1;
- }
- if (mScrollToTop)
- {
- ImGui::SetScrollY(0.0f);
- mScrollToTop = false;
- }
- if (mSetViewAtLine > -1)
- {
- float targetScroll;
- switch (mSetViewAtLineMode)
- {
- default:
- case SetViewAtLineMode::FirstVisibleLine:
- targetScroll = std::max(0.0f, (float)mSetViewAtLine * mCharAdvance.y);
- break;
- case SetViewAtLineMode::LastVisibleLine:
- targetScroll = std::max(0.0f, (float)(mSetViewAtLine - (mLastVisibleLine - mFirstVisibleLine)) * mCharAdvance.y);
- break;
- case SetViewAtLineMode::Centered:
- targetScroll = std::max(0.0f, ((float)mSetViewAtLine - (float)(mLastVisibleLine - mFirstVisibleLine) * 0.5f) * mCharAdvance.y);
- break;
- }
- ImGui::SetScrollY(targetScroll);
- mSetViewAtLine = -1;
- }
- }
- void TextEditor::OnCursorPositionChanged()
- {
- if (mState.mCurrentCursor == 0 && !mState.mCursors[0].HasSelection()) // only one cursor without selection
- mCursorOnBracket = FindMatchingBracket(mState.mCursors[0].mInteractiveEnd.mLine,
- GetCharacterIndexR(mState.mCursors[0].mInteractiveEnd), mMatchingBracketCoords);
- else
- mCursorOnBracket = false;
- if (!mDraggingSelection)
- {
- mState.SortCursorsFromTopToBottom();
- MergeCursorsIfPossible();
- }
- }
- void TextEditor::OnLineChanged(bool aBeforeChange, int aLine, int aColumn, int aCharCount, bool aDeleted) // adjusts cursor position when other cursor writes/deletes in the same line
- {
- static std::unordered_map<int, int> cursorCharIndices;
- if (aBeforeChange)
- {
- cursorCharIndices.clear();
- for (int c = 0; c <= mState.mCurrentCursor; c++)
- {
- if (mState.mCursors[c].mInteractiveEnd.mLine == aLine && // cursor is at the line
- mState.mCursors[c].mInteractiveEnd.mColumn > aColumn && // cursor is to the right of changing part
- mState.mCursors[c].GetSelectionEnd() == mState.mCursors[c].GetSelectionStart()) // cursor does not have a selection
- {
- cursorCharIndices[c] = GetCharacterIndexR({ aLine, mState.mCursors[c].mInteractiveEnd.mColumn });
- cursorCharIndices[c] += aDeleted ? -aCharCount : aCharCount;
- }
- }
- }
- else
- {
- for (auto& item : cursorCharIndices)
- SetCursorPosition({ aLine, GetCharacterColumn(aLine, item.second) }, item.first);
- }
- }
- void TextEditor::MergeCursorsIfPossible()
- {
- // requires the cursors to be sorted from top to bottom
- std::unordered_set<int> cursorsToDelete;
- if (AnyCursorHasSelection())
- {
- // merge cursors if they overlap
- for (int c = mState.mCurrentCursor; c > 0; c--)// iterate backwards through pairs
- {
- int pc = c - 1; // pc for previous cursor
- bool pcContainsC = mState.mCursors[pc].GetSelectionEnd() >= mState.mCursors[c].GetSelectionEnd();
- bool pcContainsStartOfC = mState.mCursors[pc].GetSelectionEnd() > mState.mCursors[c].GetSelectionStart();
- if (pcContainsC)
- {
- cursorsToDelete.insert(c);
- }
- else if (pcContainsStartOfC)
- {
- Coordinates pcStart = mState.mCursors[pc].GetSelectionStart();
- Coordinates cEnd = mState.mCursors[c].GetSelectionEnd();
- mState.mCursors[pc].mInteractiveEnd = cEnd;
- mState.mCursors[pc].mInteractiveStart = pcStart;
- cursorsToDelete.insert(c);
- }
- }
- }
- else
- {
- // merge cursors if they are at the same position
- for (int c = mState.mCurrentCursor; c > 0; c--)// iterate backwards through pairs
- {
- int pc = c - 1;
- if (mState.mCursors[pc].mInteractiveEnd == mState.mCursors[c].mInteractiveEnd)
- cursorsToDelete.insert(c);
- }
- }
- for (int c = mState.mCurrentCursor; c > -1; c--)// iterate backwards through each of them
- {
- if (cursorsToDelete.find(c) != cursorsToDelete.end())
- mState.mCursors.erase(mState.mCursors.begin() + c);
- }
- mState.mCurrentCursor -= cursorsToDelete.size();
- }
- void TextEditor::AddUndo(UndoRecord& aValue)
- {
- assert(!mReadOnly);
- mUndoBuffer.resize((size_t)(mUndoIndex + 1));
- mUndoBuffer.back() = aValue;
- ++mUndoIndex;
- mTextChanged = true;
- }
- // TODO
- // - multiline comments vs single-line: latter is blocking start of a ML
- 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 || mLanguageDefinition == nullptr)
- return;
- std::string buffer;
- boost::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 (const auto& p : mRegexList)
- {
- bool regexSearchResult = false;
- try { regexSearchResult = boost::regex_search(first, last, results, p.first, boost::regex_constants::match_continuous); }
- catch (...) {}
- if (regexSearchResult)
- {
- 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;
- }
- }
- }
- }
- template<class InputIt1, class InputIt2, class BinaryPredicate>
- bool ColorizerEquals(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;
- }
- void TextEditor::ColorizeInternal()
- {
- if (mLines.empty() || mLanguageDefinition == nullptr)
- 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 (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() &&
- ColorizerEquals(startStr.begin(), startStr.end(), from, from + startStr.size(), pred))
- {
- commentStartLine = currentLine;
- commentStartIndex = currentIndex;
- }
- else if (singleStartStr.size() > 0 &&
- currentIndex + singleStartStr.size() <= line.size() &&
- ColorizerEquals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.size(), pred))
- {
- withinSingleLineComment = true;
- }
- 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() &&
- ColorizerEquals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, pred))
- {
- commentStartIndex = endIndex;
- commentStartLine = endLine;
- }
- }
- }
- if (currentIndex < (int)line.size())
- 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<int>::max();
- mColorRangeMax = 0;
- }
- return;
- }
- }
- const TextEditor::Palette& TextEditor::GetDarkPalette()
- {
- const static Palette p = { {
- 0xdcdfe4ff, // Default
- 0xe06c75ff, // Keyword
- 0xe5c07bff, // Number
- 0x98c379ff, // String
- 0xe0a070ff, // Char literal
- 0x6a7384ff, // Punctuation
- 0x808040ff, // Preprocessor
- 0xdcdfe4ff, // Identifier
- 0x61afefff, // Known identifier
- 0xc678ddff, // Preproc identifier
- 0x3696a2ff, // Comment (single line)
- 0x3696a2ff, // Comment (multi line)
- 0x282c3400, // Background transparent
- //0x282c34ff, // Background
- 0xe0e0e0ff, // Cursor
- 0x2060a080, // Selection
- 0xff200080, // ErrorMarker
- 0xffffff15, // ControlCharacter
- 0x0080f040, // Breakpoint
- 0x7a8394ff, // Line number
- 0x00000040, // Current line fill
- 0x80808040, // Current line fill (inactive)
- 0xa0a0a040, // Current line edge
- } };
- return p;
- }
- const TextEditor::Palette& TextEditor::GetMarianaPalette()
- {
- const static Palette p = { {
- 0xffffffff, // Default
- 0xc695c6ff, // Keyword
- 0xf9ae58ff, // Number
- 0x99c794ff, // String
- 0xe0a070ff, // Char literal
- 0x5fb4b4ff, // Punctuation
- 0x808040ff, // Preprocessor
- 0xffffffff, // Identifier
- 0x4dc69bff, // Known identifier
- 0xe0a0ffff, // Preproc identifier
- 0xa6acb9ff, // Comment (single line)
- 0xa6acb9ff, // Comment (multi line)
- 0x303841ff, // Background
- 0xe0e0e0ff, // Cursor
- 0x6e7a8580, // Selection
- 0xec5f6680, // ErrorMarker
- 0xffffff30, // ControlCharacter
- 0x0080f040, // Breakpoint
- 0xffffffb0, // Line number
- 0x4e5a6580, // Current line fill
- 0x4e5a6530, // Current line fill (inactive)
- 0x4e5a65b0, // Current line edge
- } };
- return p;
- }
- const TextEditor::Palette& TextEditor::GetLightPalette()
- {
- const static Palette p = { {
- 0x404040ff, // None
- 0x060cffff, // Keyword
- 0x008000ff, // Number
- 0xa02020ff, // String
- 0x704030ff, // Char literal
- 0x000000ff, // Punctuation
- 0x606040ff, // Preprocessor
- 0x404040ff, // Identifier
- 0x106060ff, // Known identifier
- 0xa040c0ff, // Preproc identifier
- 0x205020ff, // Comment (single line)
- 0x205040ff, // Comment (multi line)
- 0xffffffff, // Background
- 0x000000ff, // Cursor
- 0x00006040, // Selection
- 0xff1000a0, // ErrorMarker
- 0x90909090, // ControlCharacter
- 0x0080f080, // Breakpoint
- 0x005050ff, // Line number
- 0x00000040, // Current line fill
- 0x80808040, // Current line fill (inactive)
- 0x00000040, // Current line edge
- } };
- return p;
- }
- const TextEditor::Palette& TextEditor::GetRetroBluePalette()
- {
- const static Palette p = { {
- 0xffff00ff, // None
- 0x00ffffff, // Keyword
- 0x00ff00ff, // Number
- 0x008080ff, // String
- 0x008080ff, // Char literal
- 0xffffffff, // Punctuation
- 0x008000ff, // Preprocessor
- 0xffff00ff, // Identifier
- 0xffffffff, // Known identifier
- 0xff00ffff, // Preproc identifier
- 0x808080ff, // Comment (single line)
- 0x404040ff, // Comment (multi line)
- 0x000080ff, // Background
- 0xff8000ff, // Cursor
- 0x00ffff80, // Selection
- 0xff0000a0, // ErrorMarker
- 0x0080ff80, // Breakpoint
- 0x008080ff, // Line number
- 0x00000040, // Current line fill
- 0x80808040, // Current line fill (inactive)
- 0x00000040, // Current line edge
- } };
- return p;
- }
- const std::unordered_map<char, char> TextEditor::OPEN_TO_CLOSE_CHAR = {
- {'{', '}'},
- {'(' , ')'},
- {'[' , ']'}
- };
- const std::unordered_map<char, char> TextEditor::CLOSE_TO_OPEN_CHAR = {
- {'}', '{'},
- {')' , '('},
- {']' , '['}
- };
- TextEditor::PaletteId TextEditor::defaultPalette = TextEditor::PaletteId::Dark;
|