From 4f592a8675754ba3acbc43d694e49afc0e87d546 Mon Sep 17 00:00:00 2001 From: Artyom Shalkhakov Date: Tue, 11 Feb 2025 08:43:23 -0700 Subject: [PATCH] Adding ColorTextEdit. [ColorTextEdit](https://github.com/santaclose/ImGuiColorTextEdit) commit `cd9090d`. This fork is still using C++11, but usage of `boost::regex` is removed. Let's try Zep next, but this already looks like it could do the job! --- neo/CMakeLists.txt | 5 + .../LanguageDefinitions.cpp | 937 ++++++ neo/libs/ImGuiColorTextEdit/TextEditor.cpp | 2947 +++++++++++++++++ neo/libs/ImGuiColorTextEdit/TextEditor.h | 469 +++ neo/tools/imgui/scripteditor/ScriptEditor.cpp | 40 +- neo/tools/imgui/scripteditor/ScriptEditor.h | 4 +- 6 files changed, 4382 insertions(+), 20 deletions(-) create mode 100644 neo/libs/ImGuiColorTextEdit/LanguageDefinitions.cpp create mode 100644 neo/libs/ImGuiColorTextEdit/TextEditor.cpp create mode 100644 neo/libs/ImGuiColorTextEdit/TextEditor.h diff --git a/neo/CMakeLists.txt b/neo/CMakeLists.txt index 15361ab7..8008d881 100644 --- a/neo/CMakeLists.txt +++ b/neo/CMakeLists.txt @@ -826,6 +826,10 @@ set(src_imgui ${src_imgui} libs/imgui/imgui_widgets.cpp libs/imgui/imgui_demo.cpp + + libs/ImGuiColorTextEdit/TextEditor.h + libs/ImGuiColorTextEdit/LanguageDefinitions.cpp + libs/ImGuiColorTextEdit/TextEditor.cpp sys/sys_imgui.h sys/sys_imgui.cpp @@ -1295,6 +1299,7 @@ if(CORE) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX neo FILES ${src_core} ${src_sys_base} ${src_sys_core} ${src_imgui} ${src_editor_tools}) target_include_directories(${DHEWM3BINARY} PRIVATE "${CMAKE_SOURCE_DIR}/libs/imgui") + target_include_directories(${DHEWM3BINARY} PRIVATE "${CMAKE_SOURCE_DIR}/libs/zep/include") if(HARDLINK_GAME) set_target_properties(${DHEWM3BINARY} PROPERTIES COMPILE_DEFINITIONS "${TOOLS_DEFINES}") diff --git a/neo/libs/ImGuiColorTextEdit/LanguageDefinitions.cpp b/neo/libs/ImGuiColorTextEdit/LanguageDefinitions.cpp new file mode 100644 index 00000000..4939771a --- /dev/null +++ b/neo/libs/ImGuiColorTextEdit/LanguageDefinitions.cpp @@ -0,0 +1,937 @@ +#include "TextEditor.h" + +static bool TokenizeCStyleString(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if (*p == '"') + { + p++; + + while (p < in_end) + { + // handle end of string + if (*p == '"') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && p[1] == '"') + p++; + + p++; + } + } + + return false; +} + +static bool TokenizeCStyleCharacterLiteral(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if (*p == '\'') + { + p++; + + // handle escape characters + if (p < in_end && *p == '\\') + p++; + + if (p < in_end) + p++; + + // handle end of character literal + if (p < in_end && *p == '\'') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + } + + return false; +} + +static bool TokenizeCStyleIdentifier(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') + { + p++; + + while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeCStyleNumber(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + bool isFloat = false; + bool isHex = false; + bool isBinary = false; + + if (p < in_end) + { + if (*p == '.') + { + isFloat = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + else if (*p == 'x' || *p == 'X') + { + // hex formatted integer of the type 0xef80 + + isHex = true; + + p++; + + while (p < in_end && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) + p++; + } + else if (*p == 'b' || *p == 'B') + { + // binary formatted integer of the type 0b01011101 + + isBinary = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '1')) + p++; + } + } + + if (isHex == false && isBinary == false) + { + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + isFloat = true; + + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + + // single precision floating point type + if (p < in_end && *p == 'f') + p++; + } + + if (isFloat == false) + { + // integer size type + while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) + p++; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeCStylePunctuation(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + (void)in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + } + + return false; +} + +static bool TokenizeLuaStyleString(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + bool is_single_quote = false; + bool is_double_quotes = false; + bool is_double_square_brackets = false; + + switch (*p) + { + case '\'': + is_single_quote = true; + break; + case '"': + is_double_quotes = true; + break; + case '[': + p++; + if (p < in_end && *(p) == '[') + is_double_square_brackets = true; + break; + } + + if (is_single_quote || is_double_quotes || is_double_square_brackets) + { + p++; + + while (p < in_end) + { + // handle end of string + if ((is_single_quote && *p == '\'') || (is_double_quotes && *p == '"') || (is_double_square_brackets && *p == ']' && p + 1 < in_end && *(p + 1) == ']')) + { + out_begin = in_begin; + + if (is_double_square_brackets) + out_end = p + 2; + else + out_end = p + 1; + + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && (is_single_quote || is_double_quotes)) + p++; + + p++; + } + } + + return false; +} + +static bool TokenizeLuaStyleIdentifier(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') + { + p++; + + while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeLuaStyleNumber(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + const char* p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + if (p < in_end) + { + if (*p == '.') + { + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeLuaStylePunctuation(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end) +{ + (void)in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '#': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + } + + return false; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Cpp() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const cppKeywords[] = { + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", + "compl", "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", + "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", + "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", + "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + }; + for (auto& k : cppKeywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", + "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", "max" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "C++"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Hlsl() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "AppendStructuredBuffer", "asm", "asm_fragment", "BlendState", "bool", "break", "Buffer", "ByteAddressBuffer", "case", "cbuffer", "centroid", "class", "column_major", "compile", "compile_fragment", + "CompileShader", "const", "continue", "ComputeShader", "ConsumeStructuredBuffer", "default", "DepthStencilState", "DepthStencilView", "discard", "do", "double", "DomainShader", "dword", "else", + "export", "extern", "false", "float", "for", "fxgroup", "GeometryShader", "groupshared", "half", "Hullshader", "if", "in", "inline", "inout", "InputPatch", "int", "interface", "line", "lineadj", + "linear", "LineStream", "matrix", "min16float", "min10float", "min16int", "min12int", "min16uint", "namespace", "nointerpolation", "noperspective", "NULL", "out", "OutputPatch", "packoffset", + "pass", "pixelfragment", "PixelShader", "point", "PointStream", "precise", "RasterizerState", "RenderTargetView", "return", "register", "row_major", "RWBuffer", "RWByteAddressBuffer", "RWStructuredBuffer", + "RWTexture1D", "RWTexture1DArray", "RWTexture2D", "RWTexture2DArray", "RWTexture3D", "sample", "sampler", "SamplerState", "SamplerComparisonState", "shared", "snorm", "stateblock", "stateblock_state", + "static", "string", "struct", "switch", "StructuredBuffer", "tbuffer", "technique", "technique10", "technique11", "texture", "Texture1D", "Texture1DArray", "Texture2D", "Texture2DArray", "Texture2DMS", + "Texture2DMSArray", "Texture3D", "TextureCube", "TextureCubeArray", "true", "typedef", "triangle", "triangleadj", "TriangleStream", "uint", "uniform", "unorm", "unsigned", "vector", "vertexfragment", + "VertexShader", "void", "volatile", "while", + "bool1","bool2","bool3","bool4","double1","double2","double3","double4", "float1", "float2", "float3", "float4", "int1", "int2", "int3", "int4", "in", "out", "inout", + "uint1", "uint2", "uint3", "uint4", "dword1", "dword2", "dword3", "dword4", "half1", "half2", "half3", "half4", + "float1x1","float2x1","float3x1","float4x1","float1x2","float2x2","float3x2","float4x2", + "float1x3","float2x3","float3x3","float4x3","float1x4","float2x4","float3x4","float4x4", + "half1x1","half2x1","half3x1","half4x1","half1x2","half2x2","half3x2","half4x2", + "half1x3","half2x3","half3x3","half4x3","half1x4","half2x4","half3x4","half4x4", + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "all", "AllMemoryBarrier", "AllMemoryBarrierWithGroupSync", "any", "asdouble", "asfloat", "asin", "asint", "asint", "asuint", + "asuint", "atan", "atan2", "ceil", "CheckAccessFullyMapped", "clamp", "clip", "cos", "cosh", "countbits", "cross", "D3DCOLORtoUBYTE4", "ddx", + "ddx_coarse", "ddx_fine", "ddy", "ddy_coarse", "ddy_fine", "degrees", "determinant", "DeviceMemoryBarrier", "DeviceMemoryBarrierWithGroupSync", + "distance", "dot", "dst", "errorf", "EvaluateAttributeAtCentroid", "EvaluateAttributeAtSample", "EvaluateAttributeSnapped", "exp", "exp2", + "f16tof32", "f32tof16", "faceforward", "firstbithigh", "firstbitlow", "floor", "fma", "fmod", "frac", "frexp", "fwidth", "GetRenderTargetSampleCount", + "GetRenderTargetSamplePosition", "GroupMemoryBarrier", "GroupMemoryBarrierWithGroupSync", "InterlockedAdd", "InterlockedAnd", "InterlockedCompareExchange", + "InterlockedCompareStore", "InterlockedExchange", "InterlockedMax", "InterlockedMin", "InterlockedOr", "InterlockedXor", "isfinite", "isinf", "isnan", + "ldexp", "length", "lerp", "lit", "log", "log10", "log2", "mad", "max", "min", "modf", "msad4", "mul", "noise", "normalize", "pow", "printf", + "Process2DQuadTessFactorsAvg", "Process2DQuadTessFactorsMax", "Process2DQuadTessFactorsMin", "ProcessIsolineTessFactors", "ProcessQuadTessFactorsAvg", + "ProcessQuadTessFactorsMax", "ProcessQuadTessFactorsMin", "ProcessTriTessFactorsAvg", "ProcessTriTessFactorsMax", "ProcessTriTessFactorsMin", + "radians", "rcp", "reflect", "refract", "reversebits", "round", "rsqrt", "saturate", "sign", "sin", "sincos", "sinh", "smoothstep", "sqrt", "step", + "tan", "tanh", "tex1D", "tex1D", "tex1Dbias", "tex1Dgrad", "tex1Dlod", "tex1Dproj", "tex2D", "tex2D", "tex2Dbias", "tex2Dgrad", "tex2Dlod", "tex2Dproj", + "tex3D", "tex3D", "tex3Dbias", "tex3Dgrad", "tex3Dlod", "tex3Dproj", "texCUBE", "texCUBE", "texCUBEbias", "texCUBEgrad", "texCUBElod", "texCUBEproj", "transpose", "trunc" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([ \t]*#[ \t]*[a-zA-Z_]+)##", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(L?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(\'\\?[^\']\')##", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "HLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Glsl() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([ \t]*#[ \t]*[a-zA-Z_]+)##", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(L?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(\'\\?[^\']\')##", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Python() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise", "True", "class", "finally", "is", "return", "and", "continue", "for", "lambda", "try", "as", "def", "from", "nonlocal", "while", "assert", "del", "global", "not", "with", "async", "elif", "if", "or", "yield" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abs", "aiter", "all", "any", "anext", "ascii", "bin", "bool", "breakpoint", "bytearray", "bytes", "callable", "chr", "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", "enumerate", "eval", "exec", "filter", "float", "format", "frozenset", "getattr", "globals", "hasattr", "hash", "help", "hex", "id", "input", "int", "isinstance", "issubclass", "iter", "len", "list", "locals", "map", "max", "memoryview", "min", "next", "object", "oct", "open", "ord", "pow", "print", "property", "range", "repr", "reversed", "round", "set", "setattr", "slice", "sorted", "staticmethod", "str", "sum", "super", "tuple", "type", "vars", "zip", "__import__" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##((b|u|f|r)?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##((b|u|f|r)?'(\\.|[^'])*')##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.\:])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "\"\"\""; + langDef.mCommentEnd = "\"\"\""; + langDef.mSingleLineComment = "#"; + + langDef.mCaseSensitive = true; + + langDef.mName = "Python"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "C"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Sql() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", + "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", + "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", + "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", + "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", + "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", + "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", + "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", + "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING","DROP", "OPENROWSET", "VIEW", + "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", + "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", + "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", + "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", + "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", + "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", + "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", + "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", + "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(L?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(\'[^\']*\')##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = false; + + langDef.mName = "SQL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::AngelScript() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "abstract", "auto", "bool", "break", "case", "cast", "class", "const", "continue", "default", "do", "double", "else", "enum", "false", "final", "float", "for", + "from", "funcdef", "function", "get", "if", "import", "in", "inout", "int", "interface", "int8", "int16", "int32", "int64", "is", "mixin", "namespace", "not", + "null", "or", "out", "override", "private", "protected", "return", "set", "shared", "super", "switch", "this ", "true", "typedef", "uint", "uint8", "uint16", "uint32", + "uint64", "void", "while", "xor" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "cos", "sin", "tab", "acos", "asin", "atan", "atan2", "cosh", "sinh", "tanh", "log", "log10", "pow", "sqrt", "abs", "ceil", "floor", "fraction", "closeTo", "fpFromIEEE", "fpToIEEE", + "complex", "opEquals", "opAddAssign", "opSubAssign", "opMulAssign", "opDivAssign", "opAdd", "opSub", "opMul", "opDiv" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(L?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(\'\\?[^\']\')##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "AngelScript"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION","arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug","getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeLuaStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeLuaStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeLuaStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeLuaStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "--[["; + langDef.mCommentEnd = "]]"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = true; + + langDef.mName = "Lua"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Cs() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "in (generic modifier)", "int", "interface", "internal", "is", "lock", "long", "namespace", "new", "null", "object", "operator", "out", "out (generic modifier)", "override", "params", "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "using static", "void", "volatile", "while" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "add", "alias", "ascending", "async", "await", "descending", "dynamic", "from", "get", "global", "group", "into", "join", "let", "orderby", "partial", "remove", "select", "set", "value", "var", "when", "where", "yield" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(($|@)?\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?[0-9]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[0-7]+[Uu]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([a-zA-Z_][a-zA-Z0-9_]*)##", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.])##", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "C#"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Json() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + langDef.mKeywords.clear(); + langDef.mIdentifiers.clear(); + + + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(\"(\\.|[^\"])*\")##", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?)##", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##([\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.\:])##", PaletteIndex::Punctuation)); + langDef.mTokenRegexStrings.push_back(std::make_pair(R"##(false|true)##", PaletteIndex::Keyword)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + + langDef.mName = "Json"; + + inited = true; + } + return langDef; +} \ No newline at end of file diff --git a/neo/libs/ImGuiColorTextEdit/TextEditor.cpp b/neo/libs/ImGuiColorTextEdit/TextEditor.cpp new file mode 100644 index 00000000..bc945f1b --- /dev/null +++ b/neo/libs/ImGuiColorTextEdit/TextEditor.cpp @@ -0,0 +1,2947 @@ +#include +#include +#include + +#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: + 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; + } + + 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; + + if (ImGui::GetClipboardText() == nullptr) + return; // something other than text in the clipboard + + // check if we should do multicursor paste + std::string clipText = ImGui::GetClipboardText(); + bool canPasteToMultipleCursors = false; + std::vector> clipTextLines; + if (mState.mCurrentCursor > 0) + { + clipTextLines.push_back({ 0,0 }); + for (int i = 0; i < clipText.length(); i++) + { + if (clipText[i] == '\n') + { + clipTextLines.back().second = i; + clipTextLines.push_back({ i + 1, 0 }); + } + } + clipTextLines.back().second = clipText.length(); + canPasteToMultipleCursors = clipTextLines.size() == mState.mCurrentCursor + 1; + } + + if (clipText.length() > 0) + { + UndoRecord u; + u.mBefore = mState; + + if (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_str(), c); + u.mOperations.push_back({ clipSubText, start, GetActualCursorCoordinates(c), UndoOperationType::Add }); + } + else + { + InsertTextAtCursor(clipText.c_str(), 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 +{ + auto lastLine = (int)mLines.size() - 1; + auto lastLineLength = GetLineMaxColumn(lastLine); + Coordinates startCoords = Coordinates(); + Coordinates endCoords = Coordinates(lastLine, lastLineLength); + return startCoords < endCoords ? GetText(startCoords, endCoords) : ""; +} + +void TextEditor::SetTextLines(const std::vector& aLines) +{ + mLines.clear(); + + if (aLines.empty()) + mLines.emplace_back(Line()); + else + { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) + { + const std::string& aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (size_t j = 0; j < aLine.size(); ++j) + mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); + } + } + + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +std::vector TextEditor::GetTextLines() const +{ + std::vector result; + + result.reserve(mLines.size()); + + for (auto& line : mLines) + { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) + text[i] = line[i].mChar; + + result.emplace_back(std::move(text)); + } + + return result; +} + +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& 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 Coordinates& aStart, const Coordinates& aEnd) const +{ + assert(aStart < aEnd); + + 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'; + } + } + + return result; +} + +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 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 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 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 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::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 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 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* 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; + mState.mCursors[c].mInteractiveEnd.mLine = targetLine; + } + if (mState.mCursors[c].mInteractiveStart.mLine >= aStart) + { + int targetLine = mState.mCursors[c].mInteractiveStart.mLine - (aEnd - aStart); + targetLine = targetLine < 0 ? 0 : targetLine; + mState.mCursors[c].mInteractiveStart.mLine = targetLine; + } + } +} + +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 cursor is selecting the same range we are deleting, it's because this is being called from + // DeleteSelection which already sets the cursor position after the range is deleted + if (mState.mCursors[c].GetSelectionStart() == aStart && mState.mCursors[c].GetSelectionEnd() == aEnd) + continue; + 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; + + Coordinates newCursorPos = mState.mCursors[aCursor].GetSelectionStart(); + DeleteRange(newCursorPos, mState.mCursors[aCursor].GetSelectionEnd()); + SetCursorPosition(newCursorPos, aCursor); + Colorize(newCursorPos.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(ImGuiKey_Z)) + Undo(); + else if (!mReadOnly && isAltOnly && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Undo(); + else if (!mReadOnly && isShortcut && ImGui::IsKeyPressed(ImGuiKey_Y)) + Redo(); + else if (!mReadOnly && isShiftShortcut && ImGui::IsKeyPressed(ImGuiKey_Z)) + Redo(); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUp(1, shift); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDown(1, shift); + else if ((isOSX ? !ctrl : !alt) && !super && ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) + MoveLeft(shift, isWordmoveKey); + else if ((isOSX ? !ctrl : !alt) && !super && ImGui::IsKeyPressed(ImGuiKey_RightArrow)) + MoveRight(shift, isWordmoveKey); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_PageUp)) + MoveUp(mVisibleLineCount - 2, shift); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_PageDown)) + MoveDown(mVisibleLineCount - 2, shift); + else if (ctrl && !alt && !super && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveTop(shift); + else if (ctrl && !alt && !super && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveBottom(shift); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveHome(shift); + else if (!alt && !ctrl && !super && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveEnd(shift); + else if (!mReadOnly && !alt && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Delete(ctrl); + else if (!mReadOnly && !alt && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Backspace(ctrl); + else if (!mReadOnly && !alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGuiKey_K)) + RemoveCurrentLines(); + else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) + ChangeCurrentLinesIndentation(false); + else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_RightBracket)) + ChangeCurrentLinesIndentation(true); + else if (!alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUpCurrentLines(); + else if (!alt && ctrl && shift && !super && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDownCurrentLines(); + else if (!mReadOnly && !alt && ctrl && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_Slash)) + ToggleLineComment(); + else if (!alt && !ctrl && !shift && !super && ImGui::IsKeyPressed(ImGuiKey_Insert)) + mOverwrite ^= true; + else if (isCtrlOnly && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Copy(); + else if (isShortcut && ImGui::IsKeyPressed(ImGuiKey_C)) + Copy(); + else if (!mReadOnly && isShiftOnly && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Paste(); + else if (!mReadOnly && isShortcut && ImGui::IsKeyPressed(ImGuiKey_V)) + Paste(); + else if (isShortcut && ImGui::IsKeyPressed(ImGuiKey_X)) + Cut(); + else if (isShiftOnly && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Cut(); + else if (isShortcut && ImGui::IsKeyPressed(ImGuiKey_A)) + SelectAll(); + else if (isShortcut && ImGui::IsKeyPressed(ImGuiKey_D)) + AddCursorForNextOccurrence(); + else if (!mReadOnly && !alt && !ctrl && !shift && !super && (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter))) + EnterCharacter('\n', false); + else if (!mReadOnly && !alt && !ctrl && !super && ImGui::IsKeyPressed(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, " %zu ", 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 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 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 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; +} + +// 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; + 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) + { + 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 +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::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) + 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 TextEditor::OPEN_TO_CLOSE_CHAR = { + {'{', '}'}, + {'(' , ')'}, + {'[' , ']'} +}; +const std::unordered_map TextEditor::CLOSE_TO_OPEN_CHAR = { + {'}', '{'}, + {')' , '('}, + {']' , '['} +}; + +TextEditor::PaletteId TextEditor::defaultPalette = TextEditor::PaletteId::Dark; \ No newline at end of file diff --git a/neo/libs/ImGuiColorTextEdit/TextEditor.h b/neo/libs/ImGuiColorTextEdit/TextEditor.h new file mode 100644 index 00000000..6b317ecb --- /dev/null +++ b/neo/libs/ImGuiColorTextEdit/TextEditor.h @@ -0,0 +1,469 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "imgui.h" + +class IMGUI_API TextEditor +{ +public: + // ------------- Exposed API ------------- // + + TextEditor(); + ~TextEditor(); + + enum class PaletteId + { + Dark, Light, Mariana, RetroBlue + }; + enum class LanguageDefinitionId + { + None, Cpp, C, Cs, Python, Lua, Json, Sql, AngelScript, Glsl, Hlsl + }; + enum class SetViewAtLineMode + { + FirstVisibleLine, Centered, LastVisibleLine + }; + + inline void SetReadOnlyEnabled(bool aValue) { mReadOnly = aValue; } + inline bool IsReadOnlyEnabled() const { return mReadOnly; } + inline void SetAutoIndentEnabled(bool aValue) { mAutoIndent = aValue; } + inline bool IsAutoIndentEnabled() const { return mAutoIndent; } + inline void SetShowWhitespacesEnabled(bool aValue) { mShowWhitespaces = aValue; } + inline bool IsShowWhitespacesEnabled() const { return mShowWhitespaces; } + inline void SetShowLineNumbersEnabled(bool aValue) { mShowLineNumbers = aValue; } + inline bool IsShowLineNumbersEnabled() const { return mShowLineNumbers; } + inline void SetShortTabsEnabled(bool aValue) { mShortTabs = aValue; } + inline bool IsShortTabsEnabled() const { return mShortTabs; } + inline int GetLineCount() const { return mLines.size(); } + inline bool IsOverwriteEnabled() const { return mOverwrite; } + void SetPalette(PaletteId aValue); + PaletteId GetPalette() const { return mPaletteId; } + void SetLanguageDefinition(LanguageDefinitionId aValue); + LanguageDefinitionId GetLanguageDefinition() const { return mLanguageDefinitionId; }; + const char* GetLanguageDefinitionName() const; + void SetTabSize(int aValue); + inline int GetTabSize() const { return mTabSize; } + void SetLineSpacing(float aValue); + inline float GetLineSpacing() const { return mLineSpacing; } + + inline static void SetDefaultPalette(PaletteId aValue) { defaultPalette = aValue; } + inline static PaletteId GetDefaultPalette() { return defaultPalette; } + + void SelectAll(); + void SelectLine(int aLine); + void SelectRegion(int aStartLine, int aStartChar, int aEndLine, int aEndChar); + void SelectNextOccurrenceOf(const char* aText, int aTextSize, bool aCaseSensitive = true); + void SelectAllOccurrencesOf(const char* aText, int aTextSize, bool aCaseSensitive = true); + bool AnyCursorHasSelection() const; + bool AllCursorsHaveSelection() const; + void ClearExtraCursors(); + void ClearSelections(); + void SetCursorPosition(int aLine, int aCharIndex); + inline void GetCursorPosition(int& outLine, int& outColumn) const + { + auto coords = GetActualCursorCoordinates(); + outLine = coords.mLine; + outColumn = coords.mColumn; + } + int GetFirstVisibleLine(); + int GetLastVisibleLine(); + void SetViewAtLine(int aLine, SetViewAtLineMode aMode); + + void Copy(); + void Cut(); + void Paste(); + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + inline bool CanUndo() const { return !mReadOnly && mUndoIndex > 0; }; + inline bool CanRedo() const { return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); }; + inline int GetUndoIndex() const { return mUndoIndex; }; + + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + bool Render(const char* aTitle, bool aParentIsFocused = false, const ImVec2& aSize = ImVec2(), bool aBorder = false); + + void ImGuiDebugPanel(const std::string& panelName = "Debug"); + void UnitTests(); + +private: + // ------------- Generic utils ------------- // + + static inline ImVec4 U32ColorToVec4(ImU32 in) + { + float s = 1.0f / 255.0f; + return ImVec4( + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s); + } + static inline bool IsUTFSequence(char c) + { + return (c & 0xC0) == 0x80; + } + static inline float Distance(const ImVec2& a, const ImVec2& b) + { + float x = a.x - b.x; + float y = a.y - b.y; + return sqrt(x * x + y * y); + } + template + static inline T Max(T a, T b) { return a > b ? a : b; } + template + static inline T Min(T a, T b) { return a < b ? a : b; } + + // ------------- Internal ------------- // + + enum class PaletteIndex + { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + ControlCharacter, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, + // because it is rendered as " ABC" on the screen. + struct Coordinates + { + int mLine, mColumn; + Coordinates() : mLine(0), mColumn(0) {} + Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) + { + assert(aLine >= 0); + assert(aColumn >= 0); + } + static Coordinates Invalid() { static Coordinates invalid(-1, -1); return invalid; } + + bool operator ==(const Coordinates& o) const + { + return + mLine == o.mLine && + mColumn == o.mColumn; + } + + bool operator !=(const Coordinates& o) const + { + return + mLine != o.mLine || + mColumn != o.mColumn; + } + + bool operator <(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator >(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator <=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator >=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn >= o.mColumn; + } + + Coordinates operator -(const Coordinates& o) + { + return Coordinates(mLine - o.mLine, mColumn - o.mColumn); + } + + Coordinates operator +(const Coordinates& o) + { + return Coordinates(mLine + o.mLine, mColumn + o.mColumn); + } + }; + + struct Cursor + { + Coordinates mInteractiveStart = { 0, 0 }; + Coordinates mInteractiveEnd = { 0, 0 }; + inline Coordinates GetSelectionStart() const { return mInteractiveStart < mInteractiveEnd ? mInteractiveStart : mInteractiveEnd; } + inline Coordinates GetSelectionEnd() const { return mInteractiveStart > mInteractiveEnd ? mInteractiveStart : mInteractiveEnd; } + inline bool HasSelection() const { return mInteractiveStart != mInteractiveEnd; } + }; + + struct EditorState // state to be restored with undo/redo + { + int mCurrentCursor = 0; + int mLastAddedCursor = 0; + std::vector mCursors = { {{0,0}} }; + void AddCursor(); + int GetLastAddedCursorIndex(); + void SortCursorsFromTopToBottom(); + }; + + struct Identifier + { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::unordered_map Identifiers; + typedef std::array Palette; + + struct Glyph + { + char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex), + mComment(false), mMultiLineComment(false), mPreprocessor(false) {} + }; + + typedef std::vector Line; + + struct LanguageDefinition + { + typedef std::pair TokenRegexString; + typedef bool(*TokenizeCallback)(const char* in_begin, const char* in_end, const char*& out_begin, const char*& out_end, PaletteIndex& paletteIndex); + + std::string mName; + std::unordered_set mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar = '#'; + TokenizeCallback mTokenize = nullptr; + std::vector mTokenRegexStrings; + bool mCaseSensitive = true; + + static const LanguageDefinition& Cpp(); + static const LanguageDefinition& Hlsl(); + static const LanguageDefinition& Glsl(); + static const LanguageDefinition& Python(); + static const LanguageDefinition& C(); + static const LanguageDefinition& Sql(); + static const LanguageDefinition& AngelScript(); + static const LanguageDefinition& Lua(); + static const LanguageDefinition& Cs(); + static const LanguageDefinition& Json(); + }; + + enum class UndoOperationType { Add, Delete }; + struct UndoOperation + { + std::string mText; + TextEditor::Coordinates mStart; + TextEditor::Coordinates mEnd; + UndoOperationType mType; + }; + + + class UndoRecord + { + public: + UndoRecord() {} + ~UndoRecord() {} + + UndoRecord( + const std::vector& aOperations, + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::vector mOperations; + + EditorState mBefore; + EditorState mAfter; + }; + + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + std::string GetClipboardText() const; + std::string GetSelectedText(int aCursor = -1) const; + + void SetCursorPosition(const Coordinates& aPosition, int aCursor = -1, bool aClearSelection = true); + + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void InsertTextAtCursor(const char* aValue, int aCursor = -1); + + enum class MoveDirection { Right = 0, Left = 1, Up = 2, Down = 3 }; + bool Move(int& aLine, int& aCharIndex, bool aLeft = false, bool aLockLine = false) const; + void MoveCharIndexAndColumn(int aLine, int& aCharIndex, int& aColumn) const; + void MoveCoords(Coordinates& aCoords, MoveDirection aDirection, bool aWordMode = false, int aLineCount = 1) const; + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(bool aSelect = false, bool aWordMode = false); + void MoveRight(bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(bool aWordMode = false); + void Delete(bool aWordMode = false, const EditorState* aEditorState = nullptr); + + void SetSelection(Coordinates aStart, Coordinates aEnd, int aCursor = -1); + void SetSelection(int aStartLine, int aStartChar, int aEndLine, int aEndChar, int aCursor = -1); + + void SelectNextOccurrenceOf(const char* aText, int aTextSize, int aCursor = -1, bool aCaseSensitive = true); + void AddCursorForNextOccurrence(bool aCaseSensitive = true); + bool FindNextOccurrence(const char* aText, int aTextSize, const Coordinates& aFrom, Coordinates& outStart, Coordinates& outEnd, bool aCaseSensitive = true); + bool FindMatchingBracket(int aLine, int aCharIndex, Coordinates& out); + void ChangeCurrentLinesIndentation(bool aIncrease); + void MoveUpCurrentLines(); + void MoveDownCurrentLines(); + void ToggleLineComment(); + void RemoveCurrentLines(); + + float TextDistanceToLineStart(const Coordinates& aFrom, bool aSanitizeCoords = true) const; + void EnsureCursorVisible(int aCursor = -1, bool aStartToo = false); + + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + Coordinates GetActualCursorCoordinates(int aCursor = -1, bool aStart = false) const; + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition, bool aInsertionMode = false, bool* isOverLineNumber = nullptr) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + int GetCharacterIndexL(const Coordinates& aCoordinates) const; + int GetCharacterIndexR(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetFirstVisibleCharacterIndex(int aLine) const; + int GetLineMaxColumn(int aLine, int aLimit = -1) const; + + Line& InsertLine(int aIndex); + void RemoveLine(int aIndex, const std::unordered_set* aHandledCursors = nullptr); + void RemoveLines(int aStart, int aEnd); + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + void DeleteSelection(int aCursor = -1); + + void RemoveGlyphsFromLine(int aLine, int aStartChar, int aEndChar = -1); + void AddGlyphsToLine(int aLine, int aTargetIndex, Line::iterator aSourceStart, Line::iterator aSourceEnd); + void AddGlyphToLine(int aLine, int aTargetIndex, Glyph aGlyph); + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(bool aParentIsFocused = false); + void HandleMouseInputs(); + void UpdateViewVariables(float aScrollX, float aScrollY); + void Render(bool aParentIsFocused = false); + + void OnCursorPositionChanged(); + void OnLineChanged(bool aBeforeChange, int aLine, int aColumn, int aCharCount, bool aDeleted); + void MergeCursorsIfPossible(); + + void AddUndo(UndoRecord& aValue); + + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + + std::vector mLines; + EditorState mState; + std::vector mUndoBuffer; + int mUndoIndex = 0; + + int mTabSize = 4; + float mLineSpacing = 1.0f; + bool mOverwrite = false; + bool mReadOnly = false; + bool mAutoIndent = true; + bool mShowWhitespaces = true; + bool mShowLineNumbers = true; + bool mShortTabs = false; + + int mSetViewAtLine = -1; + SetViewAtLineMode mSetViewAtLineMode; + int mEnsureCursorVisible = -1; + bool mEnsureCursorVisibleStartToo = false; + bool mScrollToTop = false; + + float mTextStart = 20.0f; // position (in pixels) where a code line starts relative to the left of the TextEditor. + int mLeftMargin = 10; + ImVec2 mCharAdvance; + float mCurrentSpaceHeight = 20.0f; + float mCurrentSpaceWidth = 20.0f; + float mLastClickTime = -1.0f; + ImVec2 mLastClickPos; + int mFirstVisibleLine = 0; + int mLastVisibleLine = 0; + int mVisibleLineCount = 0; + int mFirstVisibleColumn = 0; + int mLastVisibleColumn = 0; + int mVisibleColumnCount = 0; + float mContentWidth = 0.0f; + float mContentHeight = 0.0f; + float mScrollX = 0.0f; + float mScrollY = 0.0f; + bool mPanning = false; + bool mDraggingSelection = false; + ImVec2 mLastMousePos; + bool mCursorPositionChanged = false; + bool mCursorOnBracket = false; + Coordinates mMatchingBracketCoords; + + int mColorRangeMin = 0; + int mColorRangeMax = 0; + bool mCheckComments = true; + PaletteId mPaletteId; + Palette mPalette; + LanguageDefinitionId mLanguageDefinitionId; + const LanguageDefinition* mLanguageDefinition = nullptr; + + inline bool IsHorizontalScrollbarVisible() const { return mCurrentSpaceWidth > mContentWidth; } + inline bool IsVerticalScrollbarVisible() const { return mCurrentSpaceHeight > mContentHeight; } + inline int TabSizeAtColumn(int aColumn) const { return mTabSize - (aColumn % mTabSize); } + + static const Palette& GetDarkPalette(); + static const Palette& GetMarianaPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + static const std::unordered_map OPEN_TO_CLOSE_CHAR; + static const std::unordered_map CLOSE_TO_OPEN_CHAR; + static PaletteId defaultPalette; +}; diff --git a/neo/tools/imgui/scripteditor/ScriptEditor.cpp b/neo/tools/imgui/scripteditor/ScriptEditor.cpp index b175c3e2..75b1473e 100644 --- a/neo/tools/imgui/scripteditor/ScriptEditor.cpp +++ b/neo/tools/imgui/scripteditor/ScriptEditor.cpp @@ -26,6 +26,8 @@ If you have questions concerning this license or the applicable additional terms =========================================================================== */ +#include "../../libs/ImGuiColorTextEdit/TextEditor.h" + #include "../util/ImGui_IdWidgets.h" #include "ScriptEditor.h" @@ -70,7 +72,7 @@ ScriptEditor::ScriptEditor() : isShown( false ) , windowText() , statusBarText( "Script Editor" ) - , scriptEdit() + , scriptEdit(NULL) , okButtonEnabled( false ) , cancelButtonEnabled( true ) , findStr() @@ -81,12 +83,14 @@ ScriptEditor::ScriptEditor() , fileName() , firstLine( 0 ) { + scriptEdit = new TextEditor(); } void ScriptEditor::Reset() { windowText = "Script Editor"; statusBarText.Clear(); - scriptEdit.Clear(); + scriptEdit->SetText( std::string( "" ) ); + scriptEdit->SetTabSize( TAB_SIZE ); okButtonEnabled = false; cancelButtonEnabled = true; findStr.Clear(); @@ -96,6 +100,8 @@ void ScriptEditor::Reset() { searchForward = true; fileName.Clear(); firstLine = 0; + + UpdateStatusBar(); } void ScriptEditor::Draw() @@ -142,7 +148,8 @@ void ScriptEditor::Draw() if (clickedSelect) { } - ImGui::InputTextMultilineStr( "Text", &scriptEdit, ImVec2( 500, 500 ) ); + scriptEdit->Render( "Text", false, ImVec2( 800, 600 ) ); + UpdateStatusBar(); ImGui::BeginDisabled( !okButtonEnabled ); if ( ImGui::Button( "OK" ) ) { @@ -155,6 +162,8 @@ void ScriptEditor::Draw() showTool = false; } ImGui::EndDisabled(); + + ImGui::TextUnformatted( statusBarText.c_str() ); } ImGui::End(); @@ -186,10 +195,10 @@ ScriptEditor::UpdateStatusBar ================ */ void ScriptEditor::UpdateStatusBar( void ) { - //int line, column, character; + int line, column; - //scriptEdit.GetCursorPos( line, column, character ); - //statusBarText = idStr::Format( "Line: %d, Column: %d, Character: %d", line, column, character ); + scriptEdit->GetCursorPosition( line, column ); + statusBarText = idStr::Format( "Line: %d, Column: %d", line, column ); } /* @@ -300,12 +309,13 @@ void ScriptEditor::OpenFile( const char *fileName ) { //scriptEdit.Init(); //scriptEdit.AllowPathNames( false ); - scriptEdit.Clear(); + scriptEdit->SetText(std::string("")); idStr( fileName ).ExtractFileExtension( extension ); if ( extension.Icmp( "script" ) == 0 ) { InitScriptEvents(); + scriptEdit->SetLanguageDefinition( TextEditor::LanguageDefinitionId::Cpp ); /* scriptEdit.SetCaseSensitive( true ); scriptEdit.LoadKeyWordsFromFile( "editors/script.def" ); @@ -318,6 +328,7 @@ void ScriptEditor::OpenFile( const char *fileName ) { scriptEdit.SetStringColor(SRE_COLOR_DARK_CYAN, SRE_COLOR_LIGHT_BROWN); scriptEdit.LoadKeyWordsFromFile( "editors/gui.def" ); */ + scriptEdit->SetLanguageDefinition( TextEditor::LanguageDefinitionId::Json ); } if ( fileSystem->ReadFile( fileName, &buffer ) == -1 ) { @@ -328,12 +339,7 @@ void ScriptEditor::OpenFile( const char *fileName ) { this->fileName = fileName; - // clean up new-line crapola - scriptText.Replace( "\r", "" ); - scriptText.Replace( "\n", "\r" ); - scriptText.Replace( "\v", "\r" ); - - scriptEdit = scriptText; + scriptEdit->SetText( scriptText.c_str() ); for( const char *ptr = scriptText.c_str(); *ptr; ptr++ ) { if ( *ptr == '\r' ) { @@ -391,6 +397,7 @@ bool ScriptEditor::OnInitDialog() { //statusBar.CreateEx( SBARS_SIZEGRIP, WS_CHILD | WS_VISIBLE | CBRS_BOTTOM, initialRect, this, AFX_IDW_STATUS_BAR ); //scriptEdit.LimitText( 1024 * 1024 ); + scriptEdit->SetTabSize( TAB_SIZE ); //GetClientRect( initialRect ); @@ -591,12 +598,7 @@ void ScriptEditor::OnBnClickedOk() { common->Printf( "Writing \'%s\'...\n", fileName.c_str() ); - scriptText = scriptEdit; // scriptEdit.GetText( scriptText ); - - // clean up new-line crapola - scriptText.Replace( "\n", "" ); - scriptText.Replace( "\r", "\r\n" ); - scriptText.Replace( "\v", "\r\n" ); + scriptText = scriptEdit->GetText().c_str(); if ( fileSystem->WriteFile( fileName, scriptText, scriptText.Length(), "fs_devpath" ) == -1 ) { //MessageBox( va( "Couldn't save: %s", fileName.c_str() ), va( "Error saving: %s", fileName.c_str() ), MB_OK | MB_ICONERROR ); diff --git a/neo/tools/imgui/scripteditor/ScriptEditor.h b/neo/tools/imgui/scripteditor/ScriptEditor.h index 7563e383..b6f0a8a5 100644 --- a/neo/tools/imgui/scripteditor/ScriptEditor.h +++ b/neo/tools/imgui/scripteditor/ScriptEditor.h @@ -31,6 +31,8 @@ If you have questions concerning this license or the applicable additional terms #include "../../edit_public.h" +class TextEditor; + namespace ImGuiTools { @@ -77,7 +79,7 @@ private: bool isShown; idStr windowText; idStr statusBarText; - idStr scriptEdit; + TextEditor * scriptEdit; bool okButtonEnabled; bool cancelButtonEnabled; /*