├── .github ├── screenshot2.png └── workflows │ ├── ci.yml │ └── release.yml ├── src ├── TextEdit.Demo │ ├── SpaceMono-Regular.ttf │ ├── Shaders │ │ ├── SPIR-V │ │ │ ├── imgui-frag.spv │ │ │ ├── imgui-vertex.spv │ │ │ ├── generate-spirv.bat │ │ │ ├── imgui-frag.glsl │ │ │ └── imgui-vertex.glsl │ │ ├── Metal │ │ │ ├── imgui-frag.metallib │ │ │ ├── imgui-vertex.metallib │ │ │ ├── imgui-frag.metal │ │ │ └── imgui-vertex.metal │ │ ├── HLSL │ │ │ ├── imgui-frag.hlsl.bytes │ │ │ ├── imgui-vertex.hlsl.bytes │ │ │ ├── imgui-frag.hlsl │ │ │ └── imgui-vertex.hlsl │ │ └── GLSL │ │ │ ├── imgui-frag.glsl │ │ │ └── imgui-vertex.glsl │ ├── TextEdit.Demo.csproj │ └── Program.cs ├── TextEdit.Tests │ ├── BreakpointTests.cs │ ├── ErrorTests.cs │ ├── ClipboardTests.cs │ ├── SelectionTests.cs │ ├── TextEdit.Tests.csproj │ ├── UndoHelper.cs │ ├── BasicTests.cs │ ├── DeletionTests.cs │ ├── InsertionTests.cs │ └── MovementTests.cs ├── TextEdit │ ├── Operations │ │ ├── IEditorOperation.cs │ │ ├── RemoveLineOperation.cs │ │ ├── AddLineOperation.cs │ │ ├── MetaOperation.cs │ │ ├── ModifyLineOperation.cs │ │ └── UndoRecord.cs │ ├── SelectionState.cs │ ├── Input │ │ ├── EditorKeybindAction.cs │ │ ├── ITextEditorMouseInput.cs │ │ ├── ITextEditorKeyboardInput.cs │ │ ├── EditorKeybind.cs │ │ ├── StandardMouseInput.cs │ │ └── StandardKeyboardInput.cs │ ├── SelectionMode.cs │ ├── BreakpointRemovedEventArgs.cs │ ├── Syntax │ │ ├── ISyntaxHighlighter.cs │ │ ├── NullSyntaxHighlighter.cs │ │ ├── RegexSyntaxHighlighter.cs │ │ ├── CStyleHighlighter.cs │ │ └── LanguageDefinition.cs │ ├── Glyph.cs │ ├── Editor │ │ ├── TextEditorOptions.cs │ │ ├── TextEditorErrorMarkers.cs │ │ ├── TextEditorUndoStack.cs │ │ ├── TextEditorColor.cs │ │ ├── TextEditorBreakpoints.cs │ │ ├── TextEditorSelection.cs │ │ ├── TextEditorMovement.cs │ │ ├── TextEditorModify.cs │ │ └── TextEditorText.cs │ ├── Util.cs │ ├── SimpleCache.cs │ ├── ImGuiColorTextEditNet.csproj │ ├── PaletteIndex.cs │ ├── Line.cs │ ├── Palettes.cs │ ├── SimpleTrie.cs │ ├── Coordinates.cs │ └── TextEditor.cs └── Directory.Build.props ├── switcher.json ├── .gitignore ├── LICENSE ├── README.md └── TextEdit.sln /.github/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/.github/screenshot2.png -------------------------------------------------------------------------------- /src/TextEdit.Demo/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/SPIR-V/imgui-frag.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/SPIR-V/imgui-frag.spv -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/Metal/imgui-frag.metallib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/Metal/imgui-frag.metallib -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/SPIR-V/imgui-vertex.spv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/SPIR-V/imgui-vertex.spv -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/HLSL/imgui-frag.hlsl.bytes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/HLSL/imgui-frag.hlsl.bytes -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/Metal/imgui-vertex.metallib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/Metal/imgui-vertex.metallib -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/HLSL/imgui-vertex.hlsl.bytes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csinkers/ImGuiColorTextEditNet/HEAD/src/TextEdit.Demo/Shaders/HLSL/imgui-vertex.hlsl.bytes -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/SPIR-V/generate-spirv.bat: -------------------------------------------------------------------------------- 1 | glslangvalidator -V imgui-vertex.glsl -o imgui-vertex.spv -S vert 2 | glslangvalidator -V imgui-frag.glsl -o imgui-frag.spv -S frag 3 | -------------------------------------------------------------------------------- /switcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": "TextEdit.sln", 3 | "solutionFolder": null, 4 | "mappings": { 5 | "ImGui.NET": "../ImGui.NET/src/ImGui.NET/ImGui.NET.csproj" 6 | }, 7 | "removeProjects": true 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | /BenchmarkDotNet.Artifacts 4 | build/ 5 | deps/ 6 | packages/ 7 | 8 | *.lock 9 | *.lock~ 10 | *.user 11 | *.DotSettings 12 | *.code-workspace 13 | *.gfxr 14 | launchSettings.json 15 | imgui.ini 16 | 17 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/BreakpointTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace TextEdit.Tests; 4 | 5 | [TestClass] 6 | public class BreakpointTests 7 | { 8 | // control.SetBreakpoints(new HashSet()); 9 | } 10 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/ErrorTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace TextEdit.Tests; 4 | 5 | [TestClass] 6 | public class ErrorTests 7 | { 8 | // control.SetErrorMarkers(new Dictionary()); 9 | } 10 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/ClipboardTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace TextEdit.Tests; 4 | 5 | [TestClass] 6 | public class ClipboardTests 7 | { 8 | // control.Cut(); 9 | // control.Copy(); 10 | // control.Paste(); 11 | } 12 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/GLSL/imgui-frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform sampler2D FontTexture; 4 | 5 | in vec4 color; 6 | in vec2 texCoord; 7 | 8 | out vec4 outputColor; 9 | 10 | void main() 11 | { 12 | outputColor = color * texture(FontTexture, texCoord); 13 | } 14 | -------------------------------------------------------------------------------- /src/TextEdit/Operations/IEditorOperation.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Operations; 2 | 3 | internal interface IEditorOperation 4 | { 5 | void Apply(TextEditor editor); 6 | void Undo(TextEditor editor); 7 | object SerializeState(); // Currently just used for unit test assertions 8 | } 9 | -------------------------------------------------------------------------------- /src/TextEdit/SelectionState.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet; 2 | 3 | internal struct SelectionState 4 | { 5 | public Coordinates Start; 6 | public Coordinates End; 7 | public Coordinates Cursor; 8 | 9 | public override string ToString() => $"SEL [{Start}-{End}] CUR {Cursor}"; 10 | } 11 | -------------------------------------------------------------------------------- /src/TextEdit/Input/EditorKeybindAction.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Input; 2 | 3 | /// 4 | /// Delegate type for actions that are invoked due to a key binding. 5 | /// 6 | /// True if the key was handled 7 | public delegate bool EditorKeybindAction(TextEditor editor, object? context); 8 | -------------------------------------------------------------------------------- /src/TextEdit/Input/ITextEditorMouseInput.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Input; 2 | 3 | /// 4 | /// Defines the interface for handling mouse inputs. 5 | /// 6 | public interface ITextEditorMouseInput 7 | { 8 | /// Handles ImGui mouse inputs for the text editor. 9 | void HandleMouseInputs(); 10 | } 11 | -------------------------------------------------------------------------------- /src/TextEdit/Input/ITextEditorKeyboardInput.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Input; 2 | 3 | /// 4 | /// Defines the interface for handling keyboard inputs. 5 | /// 6 | public interface ITextEditorKeyboardInput 7 | { 8 | /// Handles ImGui keyboard inputs for the text editor 9 | void HandleKeyboardInputs(); 10 | } 11 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/HLSL/imgui-frag.hlsl: -------------------------------------------------------------------------------- 1 | struct PS_INPUT 2 | { 3 | float4 pos : SV_POSITION; 4 | float4 col : COLOR0; 5 | float2 uv : TEXCOORD0; 6 | }; 7 | 8 | Texture2D FontTexture : register(t0); 9 | sampler FontSampler : register(s0); 10 | 11 | float4 FS(PS_INPUT input) : SV_Target 12 | { 13 | float4 out_col = input.col * FontTexture.Sample(FontSampler, input.uv); 14 | return out_col; 15 | } -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/GLSL/imgui-vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | uniform ProjectionMatrixBuffer 4 | { 5 | mat4 projection_matrix; 6 | }; 7 | 8 | in vec2 in_position; 9 | in vec2 in_texCoord; 10 | in vec4 in_color; 11 | 12 | out vec4 color; 13 | out vec2 texCoord; 14 | 15 | void main() 16 | { 17 | gl_Position = projection_matrix * vec4(in_position, 0, 1); 18 | color = in_color; 19 | texCoord = in_texCoord; 20 | } 21 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/Metal/imgui-frag.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct PS_INPUT 5 | { 6 | float4 pos [[ position ]]; 7 | float4 col; 8 | float2 uv; 9 | }; 10 | 11 | fragment float4 FS( 12 | PS_INPUT input [[ stage_in ]], 13 | texture2d FontTexture [[ texture(0) ]], 14 | sampler FontSampler [[ sampler(0) ]]) 15 | { 16 | float4 out_col = input.col * FontTexture.sample(FontSampler, input.uv); 17 | return out_col; 18 | } -------------------------------------------------------------------------------- /src/TextEdit.Tests/SelectionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace TextEdit.Tests; 4 | 5 | [TestClass] 6 | public class SelectionTests 7 | { 8 | // control.HasSelection; 9 | // control.GetSelectedText(); 10 | // control.GetCurrentLineText(); 11 | // control.SelectAll(); 12 | // control.SelectWordUnderCursor(); 13 | // control.SetSelection(); 14 | // control.SetSelectionStart(); 15 | // control.SetSelectionEnd(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)\..\build\$(MSBuildProjectName)\bin 5 | $(MSBuildThisFileDirectory)\..\build\$(MSBuildProjectName)\obj 6 | $(BaseIntermediateOutputPath)\$(Configuration) 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/SPIR-V/imgui-frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | #extension GL_ARB_separate_shader_objects : enable 4 | #extension GL_ARB_shading_language_420pack : enable 5 | 6 | layout(set = 1, binding = 0) uniform texture2D FontTexture; 7 | layout(set = 0, binding = 1) uniform sampler FontSampler; 8 | 9 | layout (location = 0) in vec4 color; 10 | layout (location = 1) in vec2 texCoord; 11 | layout (location = 0) out vec4 outputColor; 12 | 13 | void main() 14 | { 15 | outputColor = color * texture(sampler2D(FontTexture, FontSampler), texCoord); 16 | } -------------------------------------------------------------------------------- /src/TextEdit/SelectionMode.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet; 2 | 3 | /// Defines the selection modes available. 4 | public enum SelectionMode 5 | { 6 | /// Selects text by characters, allowing for precise selection of individual characters. 7 | Normal, 8 | 9 | /// Selects text by words, allowing for quick selection of entire words at once. 10 | Word, 11 | 12 | /// Selects text by lines, allowing for quick selection of entire lines of text. 13 | Line, 14 | } 15 | -------------------------------------------------------------------------------- /src/TextEdit/BreakpointRemovedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet; 4 | 5 | /// Event arguments for when a breakpoint is removed. 6 | public class BreakpointRemovedEventArgs : EventArgs 7 | { 8 | /// Initializes a new instance of the class with the specified context. 9 | public BreakpointRemovedEventArgs(object context) => Context = context; 10 | 11 | /// Gets the context associated with the breakpoint removal event. 12 | public object Context { get; } 13 | } 14 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/HLSL/imgui-vertex.hlsl: -------------------------------------------------------------------------------- 1 | cbuffer ProjectionMatrixBuffer : register(b0) 2 | { 3 | float4x4 ProjectionMatrix; 4 | }; 5 | 6 | struct VS_INPUT 7 | { 8 | float2 pos : POSITION; 9 | float2 uv : TEXCOORD0; 10 | float4 col : COLOR0; 11 | }; 12 | 13 | struct PS_INPUT 14 | { 15 | float4 pos : SV_POSITION; 16 | float4 col : COLOR0; 17 | float2 uv : TEXCOORD0; 18 | }; 19 | 20 | PS_INPUT VS(VS_INPUT input) 21 | { 22 | PS_INPUT output; 23 | output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f)); 24 | output.col = input.col; 25 | output.uv = input.uv; 26 | return output; 27 | } -------------------------------------------------------------------------------- /src/TextEdit/Operations/RemoveLineOperation.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Operations; 2 | 3 | internal class RemoveLineOperation : IEditorOperation 4 | { 5 | public int Line; 6 | public string Removed = ""; 7 | 8 | public void Apply(TextEditor editor) 9 | { 10 | editor.Text.RemoveLine(Line); 11 | editor.Color.InvalidateColor(Line - 1, 2); 12 | } 13 | 14 | public void Undo(TextEditor editor) 15 | { 16 | editor.Text.InsertLine(Line, Removed); 17 | editor.Color.InvalidateColor(Line - 1, 2); 18 | } 19 | 20 | public object SerializeState() 21 | { 22 | return new { Line, Removed }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/Metal/imgui-vertex.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct VS_INPUT 5 | { 6 | float2 pos [[ attribute(0) ]]; 7 | float2 uv [[ attribute(1) ]]; 8 | float4 col [[ attribute(2) ]]; 9 | }; 10 | 11 | struct PS_INPUT 12 | { 13 | float4 pos [[ position ]]; 14 | float4 col; 15 | float2 uv; 16 | }; 17 | 18 | vertex PS_INPUT VS( 19 | VS_INPUT input [[ stage_in ]], 20 | constant float4x4 &ProjectionMatrix [[ buffer(1) ]]) 21 | { 22 | PS_INPUT output; 23 | output.pos = ProjectionMatrix * float4(input.pos.xy, 0.f, 1.f); 24 | output.col = input.col; 25 | output.uv = input.uv; 26 | return output; 27 | } -------------------------------------------------------------------------------- /src/TextEdit.Tests/TextEdit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/TextEdit/Operations/AddLineOperation.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet.Operations; 2 | 3 | internal class AddLineOperation : IEditorOperation 4 | { 5 | public int InsertBeforeLine; 6 | public string Added = ""; 7 | 8 | public void Apply(TextEditor editor) 9 | { 10 | editor.Text.InsertLine(InsertBeforeLine, Added); 11 | editor.Color.InvalidateColor(InsertBeforeLine - 1, 2); 12 | } 13 | 14 | public void Undo(TextEditor editor) 15 | { 16 | editor.Text.RemoveLine(InsertBeforeLine); 17 | editor.Color.InvalidateColor(InsertBeforeLine - 1, 2); 18 | } 19 | 20 | public object SerializeState() 21 | { 22 | return new { InsertBeforeLine, Added }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Shaders/SPIR-V/imgui-vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | #extension GL_ARB_separate_shader_objects : enable 4 | #extension GL_ARB_shading_language_420pack : enable 5 | 6 | layout (location = 0) in vec2 in_position; 7 | layout (location = 1) in vec2 in_texCoord; 8 | layout (location = 2) in vec4 in_color; 9 | 10 | layout (binding = 0) uniform ProjectionMatrixBuffer 11 | { 12 | mat4 projection_matrix; 13 | }; 14 | 15 | layout (location = 0) out vec4 color; 16 | layout (location = 1) out vec2 texCoord; 17 | 18 | out gl_PerVertex 19 | { 20 | vec4 gl_Position; 21 | }; 22 | 23 | void main() 24 | { 25 | gl_Position = projection_matrix * vec4(in_position, 0, 1); 26 | color = in_color; 27 | texCoord = in_texCoord; 28 | } 29 | -------------------------------------------------------------------------------- /src/TextEdit/Syntax/ISyntaxHighlighter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Syntax; 4 | 5 | /// 6 | /// Defines the interface for syntax highlighters. 7 | /// 8 | public interface ISyntaxHighlighter 9 | { 10 | /// Indicates whether the highlighter supports auto-indentation. 11 | bool AutoIndentation { get; } 12 | 13 | /// The maximum number of lines that can be processed in a single frame. 14 | int MaxLinesPerFrame { get; } 15 | 16 | /// Retrieves the tooltip for a given identifier. 17 | string? GetTooltip(string id); 18 | 19 | /// Colorizes a line of text. 20 | object Colorize(Span line, object? state); 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | env: 9 | DOTNET_NOLOGO: true 10 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [macOS-latest, ubuntu-latest, windows-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v4.2.2 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4.3.0 24 | with: 25 | dotnet-version: '8.0.x' 26 | - name: Build with dotnet 27 | run: dotnet build --configuration Release src/TextEdit/ImGuiColorTextEditNet.csproj 28 | - name: Test with dotnet 29 | run: dotnet test TextEdit.sln --configuration Release 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: ["*.*.*"] 6 | 7 | env: 8 | DOTNET_NOLOGO: true 9 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4.2.2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4.3.0 18 | with: 19 | dotnet-version: '8.0.x' 20 | 21 | - name: Build with dotnet 22 | env: 23 | MINVER_VERSION: ${{ vars.MINVER_VERSION }} 24 | run: dotnet build --configuration Release src/TextEdit/ImGuiColorTextEditNet.csproj 25 | 26 | - name: Push to NuGet 27 | env: 28 | SOURCE: https://api.nuget.org/v3/index.json 29 | API_KEY: ${{ secrets.NUGET_API_KEY }} 30 | if: env.SOURCE != '' || env.API_KEY != '' 31 | run: dotnet nuget push ./build/ImGuiColorTextEditNet/bin/Release/*.nupkg --source ${{ env.SOURCE }} --api-key ${{ env.API_KEY }} 32 | 33 | -------------------------------------------------------------------------------- /src/TextEdit/Glyph.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet; 2 | 3 | /// 4 | /// Represents a single character, along with its associated color index. 5 | /// 6 | public readonly struct Glyph 7 | { 8 | /// The character represented by this glyph. 9 | public readonly char Char; 10 | 11 | /// The color index associated with this glyph, used to determine its display color. 12 | public readonly PaletteIndex ColorIndex = PaletteIndex.Default; 13 | 14 | /// Returns a string representation of the glyph, including its character and color index. 15 | public override string ToString() => $"{Char} {ColorIndex}"; 16 | 17 | /// 18 | /// Initializes a new instance of the struct with the specified character and default color index. 19 | /// 20 | public Glyph(char aChar, PaletteIndex aColorIndex) 21 | { 22 | Char = aChar; 23 | ColorIndex = aColorIndex; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CSinkers (C# port) 4 | Copyright (c) 2017 BalazsJako (original project) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/TextEdit/Syntax/NullSyntaxHighlighter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Syntax; 4 | 5 | /// A syntax highlighter that does not perform any syntax highlighting. 6 | public class NullSyntaxHighlighter : ISyntaxHighlighter 7 | { 8 | static readonly object DefaultState = new(); 9 | 10 | NullSyntaxHighlighter() { } 11 | 12 | /// Singleton instance of the class. 13 | public static NullSyntaxHighlighter Instance { get; } = new(); 14 | 15 | /// Indicates whether the highlighter supports auto-indentation. 16 | public bool AutoIndentation { get; init; } 17 | 18 | /// The maximum number of lines that can be processed in a single frame. 19 | public int MaxLinesPerFrame { get; init; } = 1000; 20 | 21 | /// Retrieves the tooltip for a given identifier. Since this is a null highlighter, it returns null. 22 | public string? GetTooltip(string id) => null; 23 | 24 | /// Colorizes a line of text. Since this is a null highlighter, it does not perform any colorization and returns the default state. 25 | public object Colorize(Span line, object? state) => DefaultState; 26 | } 27 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Editor; 4 | 5 | /// 6 | /// Represents options for configuring the behavior and appearance of the text editor. 7 | /// 8 | public class TextEditorOptions 9 | { 10 | int _tabSize = 4; 11 | 12 | /// 13 | /// Whether the text editor is read-only or allows editing. 14 | /// 15 | public bool IsReadOnly { get; set; } 16 | 17 | /// 18 | /// Whether the editor is in overwrite or insert mode. 19 | /// 20 | public bool IsOverwrite { get; set; } 21 | 22 | /// 23 | /// Whether the editor should colorize text using a syntax highlighter. 24 | /// 25 | public bool IsColorizerEnabled { get; set; } = true; 26 | 27 | /// 28 | /// Whether to insert spaces or a tab character when indenting. 29 | /// 30 | public bool IndentWithSpaces { get; set; } 31 | 32 | /// 33 | /// The number of spaces to use for a tab character. 34 | /// 35 | public int TabSize 36 | { 37 | get => _tabSize; 38 | set => _tabSize = Math.Max(1, Math.Min(32, value)); 39 | } 40 | 41 | internal int NextTab(int column) => column / TabSize * TabSize + TabSize; 42 | } 43 | -------------------------------------------------------------------------------- /src/TextEdit/Operations/MetaOperation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ImGuiColorTextEditNet.Operations; 6 | 7 | internal class MetaOperation : IEditorOperation 8 | { 9 | readonly List _operations = []; 10 | 11 | public SelectionState Before; 12 | public SelectionState After; 13 | public int Count => _operations.Count; 14 | 15 | public void Add(IEditorOperation operation) 16 | { 17 | ArgumentNullException.ThrowIfNull(operation); 18 | _operations.Add(operation); 19 | } 20 | 21 | public void Apply(TextEditor editor) 22 | { 23 | foreach (var op in _operations) 24 | op.Apply(editor); 25 | 26 | editor.Selection.Select(After.Start, After.End); 27 | editor.Selection.Cursor = After.Cursor; 28 | editor.Text.PendingScrollRequest = editor.Selection.Cursor.Line; 29 | } 30 | 31 | public void Undo(TextEditor editor) 32 | { 33 | foreach (var op in _operations.AsEnumerable().Reverse()) 34 | op.Undo(editor); 35 | 36 | editor.Selection.Select(Before.Start, Before.End); 37 | editor.Selection.Cursor = Before.Cursor; 38 | editor.Text.PendingScrollRequest = editor.Selection.Cursor.Line; 39 | } 40 | 41 | public object SerializeState() => _operations.Select(op => op.SerializeState()).ToList(); 42 | } 43 | -------------------------------------------------------------------------------- /src/TextEdit/Util.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ImGuiColorTextEditNet; 5 | 6 | /// 7 | /// Utility class for assertion handling. 8 | /// 9 | public static class Util 10 | { 11 | /// 12 | /// Delegate for assertion handlers. 13 | /// 14 | public delegate void AssertionHandlerDelegate( 15 | bool condition, 16 | string? expression, 17 | string? file, 18 | int line, 19 | string? method 20 | ); 21 | 22 | /// 23 | /// The active assertion handler 24 | /// 25 | public static AssertionHandlerDelegate AssertionHandler { get; set; } = DefaultHandler; 26 | 27 | static void DefaultHandler( 28 | bool condition, 29 | string? expression, 30 | string? file, 31 | int line, 32 | string? method 33 | ) => Debug.Assert(expression != null); 34 | 35 | /// 36 | /// Asserts that a condition is true by calling the AssertionHandler delegate. 37 | /// 38 | public static void Assert( 39 | bool condition, 40 | [CallerArgumentExpression("condition")] string? expression = null, 41 | [CallerFilePath] string? file = null, 42 | [CallerLineNumber] int line = 0, 43 | [CallerMemberName] string? method = null 44 | ) => AssertionHandler(condition, expression, file, line, method); 45 | } 46 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/UndoHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImGuiColorTextEditNet; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace TextEdit.Tests; 6 | 7 | internal static class UndoHelper 8 | { 9 | /// 10 | /// Tests undo handling for an operation that can be undone. 11 | /// 12 | public static void TestUndo(TextEditor editor, Action func) 13 | { 14 | var initialState = editor.SerializeState(); 15 | var initialUndo = editor.UndoStack.SerializeState(); 16 | long v = editor.Version; 17 | 18 | func(editor); 19 | var afterState = editor.SerializeState(); 20 | var afterUndo = editor.UndoStack.SerializeState(); 21 | 22 | Assert.AreNotEqual(initialUndo, afterUndo); // Verify that an undo record is created. 23 | Assert.AreNotEqual(v, editor.Version); 24 | v = editor.Version; 25 | 26 | editor.Undo(); 27 | var undoState = editor.SerializeState(); 28 | Assert.AreNotEqual(v, editor.Version); 29 | v = editor.Version; 30 | 31 | editor.Redo(); 32 | var redoState = editor.SerializeState(); 33 | Assert.AreNotEqual(v, editor.Version); 34 | 35 | Assert.AreEqual(initialState, undoState); 36 | Assert.AreEqual(afterState, redoState); 37 | } 38 | 39 | /// 40 | /// Tests undo handling for an operation that should not result in an undo record. 41 | /// 42 | public static void TestNopUndo(TextEditor editor, Action func) 43 | { 44 | var initialUndo = editor.UndoStack.SerializeState(); 45 | func(editor); 46 | var afterUndo = editor.UndoStack.SerializeState(); 47 | 48 | Assert.AreEqual(initialUndo, afterUndo); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/TextEdit/Operations/ModifyLineOperation.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace ImGuiColorTextEditNet.Operations; 4 | 5 | internal class ModifyLineOperation : IEditorOperation 6 | { 7 | public int Line; 8 | public int AddedColumn = 0; 9 | public string Added = ""; 10 | public int RemovedColumn = 0; 11 | public string Removed = ""; 12 | 13 | public void Apply(TextEditor editor) 14 | { 15 | if (!string.IsNullOrEmpty(Removed)) 16 | editor.Text.RemoveInLine(Line, RemovedColumn, RemovedColumn + Removed.Length); 17 | 18 | if (!string.IsNullOrEmpty(Added)) 19 | editor.Text.InsertTextAt((Line, AddedColumn), Added); 20 | 21 | editor.Color.InvalidateColor(Line - 1, 1); 22 | } 23 | 24 | public void Undo(TextEditor editor) 25 | { 26 | if (!string.IsNullOrEmpty(Added)) 27 | editor.Text.RemoveInLine(Line, AddedColumn, AddedColumn + Added.Length); 28 | 29 | if (!string.IsNullOrEmpty(Removed)) 30 | editor.Text.InsertTextAt((Line, RemovedColumn), Removed); 31 | 32 | editor.Color.InvalidateColor(Line - 1, 1); 33 | } 34 | 35 | public object SerializeState() => 36 | new 37 | { 38 | Line, 39 | AddedColumn, 40 | Added, 41 | RemovedColumn, 42 | Removed, 43 | }; 44 | 45 | public override string ToString() 46 | { 47 | var sb = new StringBuilder(); 48 | sb.Append($"Mod{Line}:"); 49 | 50 | if (!string.IsNullOrEmpty(Removed)) 51 | { 52 | sb.Append("-\""); 53 | sb.Append(Removed); 54 | sb.Append("\" @ "); 55 | sb.Append(RemovedColumn); 56 | } 57 | 58 | if (!string.IsNullOrEmpty(Added)) 59 | { 60 | sb.Append("+\""); 61 | sb.Append(Added); 62 | sb.Append("\" @ "); 63 | sb.Append(AddedColumn); 64 | } 65 | 66 | return sb.ToString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/TextEdit/SimpleCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ImGuiColorTextEditNet; 5 | 6 | internal class SimpleCache 7 | where TKey : notnull 8 | { 9 | const int CyclePeriod = 500; 10 | 11 | readonly string _name; 12 | readonly Func _valueBuilder; 13 | 14 | Dictionary _cache = new(); 15 | Dictionary _lastCache = new(); 16 | int _cycleCounter; 17 | 18 | #if DEBUG 19 | int _hits; 20 | int _requests; 21 | 22 | public override string ToString() => 23 | $"Cache for {_name}. Hit rate {100.0f * _hits / _requests:F1}%, size {_cache.Count} + {_lastCache.Count}"; 24 | #else 25 | public override string ToString() => $"Cache for {_name}"; 26 | #endif 27 | 28 | public SimpleCache(string name, Func valueBuilder) 29 | { 30 | _name = name; 31 | _valueBuilder = valueBuilder ?? throw new ArgumentNullException(nameof(valueBuilder)); 32 | } 33 | 34 | public TValue Get(TKey value) 35 | { 36 | #if DEBUG 37 | _requests++; 38 | #endif 39 | if (_cycleCounter++ >= CyclePeriod) 40 | { 41 | _cycleCounter = 0; 42 | if (_cache.Count > 2 * _lastCache.Count || _lastCache.Count > 2 * _cache.Count) 43 | { 44 | // Console.WriteLine($"Cache {_name} cycling ({_lastCache.Count} -> {_cache.Count})"); 45 | _lastCache = _cache; 46 | _cache = new(); 47 | } 48 | } 49 | 50 | if (_cache.TryGetValue(value, out var label)) 51 | { 52 | #if DEBUG 53 | _hits++; 54 | #endif 55 | return label; 56 | } 57 | 58 | if (_lastCache.TryGetValue(value, out label)) 59 | { 60 | #if DEBUG 61 | _hits++; 62 | #endif 63 | _cache[value] = label; 64 | return label; 65 | } 66 | 67 | label = _valueBuilder(value); 68 | _cache[value] = label; 69 | return label; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TextEdit/Input/EditorKeybind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImGuiNET; 3 | 4 | namespace ImGuiColorTextEditNet.Input; 5 | 6 | /// 7 | /// Details of an editor key-bind 8 | /// 9 | public record struct EditorKeybind(bool Shift, bool Ctrl, ImGuiKey Key) 10 | { 11 | /// 12 | /// Parses a string representation of a key-bind, e.g. Ctrl+S. 13 | /// 14 | public static bool TryParse(string s, out EditorKeybind result) 15 | { 16 | bool shift = false; 17 | bool ctrl = false; 18 | ImGuiKey? key = null; 19 | 20 | var parts = s.Split(['+', ' '], StringSplitOptions.RemoveEmptyEntries); 21 | foreach (var part in parts) 22 | { 23 | var p = part.Trim(); 24 | switch (p.ToLowerInvariant()) 25 | { 26 | case "ctrl": 27 | case "control": 28 | ctrl = true; 29 | break; 30 | 31 | case "shift": 32 | shift = true; 33 | break; 34 | 35 | default: 36 | if (key != null) 37 | { 38 | result = default; 39 | return false; 40 | } 41 | if (p.Length == 1) // Try to parse as a single character (A-Z, 0-9) 42 | { 43 | char c = char.ToUpperInvariant(p[0]); 44 | if (c is >= 'A' and <= 'Z') 45 | key = ImGuiKey.A + (c - 'A'); 46 | else if (c is >= '0' and <= '9') 47 | key = ImGuiKey._0 + (c - '0'); 48 | } 49 | 50 | // Try to parse as a named key 51 | if (Enum.TryParse(p, ignoreCase: true, out var temp)) 52 | key = temp; 53 | 54 | break; 55 | } 56 | } 57 | 58 | if (key == null) 59 | { 60 | result = default; 61 | return false; 62 | } 63 | 64 | result = new EditorKeybind(shift, ctrl, key.Value); 65 | return true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImGuiColorTextEditNet 2 | C# port of [syntax highlighting text editor for ImGui](https://github.com/BalazsJako/ImGuiColorTextEdit) 3 | ![Screenshot](/.github/screenshot2.png?raw=true) 4 | 5 | # C# Specific notes: 6 | - The way the syntax highlighting works has been changed 7 | - Regex-based syntax highlighting hasn't been ported yet 8 | - There's likely to still be a few regressions from the porting process 9 | - A small demo project using veldrid is provided. 10 | 11 | # Description from original project: 12 | This started as my attempt to write a relatively simple widget which provides text editing functionality with syntax highlighting. Now there are other contributors who provide valuable additions. 13 | 14 | While it relies on Omar Cornut's https://github.com/ocornut/imgui, it does not follow the "pure" one widget - one function approach. Since the editor has to maintain a relatively complex and large internal state, it did not seem to be practical to try and enforce fully immediate mode. It stores its internal state in an object instance which is reused across frames. 15 | 16 | The code is (still) work in progress, please report if you find any issues. 17 | 18 | # Main features 19 | - approximates typical code editor look and feel (essential mouse/keyboard commands work - I mean, the commands _I_ normally use :)) 20 | - undo/redo 21 | - UTF-8 support 22 | - works with both fixed and variable-width fonts 23 | - extensible syntax highlighting for multiple languages 24 | - identifier declarations: a small piece of description can be associated with an identifier. The editor displays it in a tooltip when the mouse cursor is hovered over the identifier 25 | - error markers: the user can specify a list of error messages together the line of occurence, the editor will highligh the lines with red backround and display error message in a tooltip when the mouse cursor is hovered over the line 26 | - large files: there is no explicit limit set on file size or number of lines (below 2GB, performance is not affected when large files are loaded (except syntax coloring, see below) 27 | - color palette support: you can switch between different color palettes, or even define your own 28 | - whitespace indicators (TAB, space) 29 | 30 | -------------------------------------------------------------------------------- /TextEdit.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiColorTextEditNet", "src\TextEdit\ImGuiColorTextEditNet.csproj", "{470F68F3-B89D-4BA0-A3FD-35C5F252B43C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextEdit.Demo", "src\TextEdit.Demo\TextEdit.Demo.csproj", "{2EA4B6F7-B373-40B7-8D95-A1AC001FEA38}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextEdit.Tests", "src\TextEdit.Tests\TextEdit.Tests.csproj", "{65970BAE-B6B5-4F31-8652-00D34F84F398}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {470F68F3-B89D-4BA0-A3FD-35C5F252B43C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {470F68F3-B89D-4BA0-A3FD-35C5F252B43C}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {470F68F3-B89D-4BA0-A3FD-35C5F252B43C}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {470F68F3-B89D-4BA0-A3FD-35C5F252B43C}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {2EA4B6F7-B373-40B7-8D95-A1AC001FEA38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {2EA4B6F7-B373-40B7-8D95-A1AC001FEA38}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {2EA4B6F7-B373-40B7-8D95-A1AC001FEA38}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {2EA4B6F7-B373-40B7-8D95-A1AC001FEA38}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {65970BAE-B6B5-4F31-8652-00D34F84F398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {65970BAE-B6B5-4F31-8652-00D34F84F398}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {65970BAE-B6B5-4F31-8652-00D34F84F398}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {65970BAE-B6B5-4F31-8652-00D34F84F398}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {A3B39D4A-55A9-4F1D-A44E-6BC49D68CCD8} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/TextEdit/ImGuiColorTextEditNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 12 5 | enable 6 | ImGuiColorTextEditNet 7 | Cam Sinclair, BalazsJako 8 | A multi-line ImGui text editor control supporting syntax highlighting 9 | 2023 Cam Sinclair 10 | true 11 | ImGuiColorTextEditNet 12 | MIT 13 | https://github.com/csinkers/ImGuiColorTextEditNet 14 | imgui 15 | README.md 16 | true 17 | https://github.com/csinkers/ImGuiColorTextEditNet 18 | Git 19 | True 20 | true 21 | Embedded 22 | 23 | 24 | 25 | True 26 | \ 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 0 39 | $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) 40 | 41 | 42 | 43 | 44 | <_Parameter1>TextEdit.Tests 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/TextEdit.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 35 | 36 | 37 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/TextEdit/PaletteIndex.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet; 2 | 3 | /// Defines the color palette indices used for syntax highlighting. 4 | public enum PaletteIndex : ushort 5 | { 6 | /// The default color index used for text that does not match any specific syntax highlighting rules. 7 | Default, 8 | 9 | /// The color for keywords in the language (e.g., `if`, `else`, `while`). 10 | Keyword, 11 | 12 | /// The color for numbers. 13 | Number, 14 | 15 | /// The color for string literals. 16 | String, 17 | 18 | /// The color for character literals. 19 | CharLiteral, 20 | 21 | /// The color for operators, punctuation marks etc. 22 | Punctuation, 23 | 24 | /// The color for preprocessor directives. 25 | Preprocessor, 26 | 27 | /// The color for identifiers (variable names, type names etc.) 28 | Identifier, 29 | 30 | /// The color for known identifiers (e.g. build in functions). 31 | KnownIdentifier, 32 | 33 | /// The color for preprocessor identifiers. 34 | PreprocIdentifier, 35 | 36 | /// The color for single-line comments. 37 | Comment, 38 | 39 | /// The color for multi-line comments. 40 | MultiLineComment, 41 | 42 | /// The background color. 43 | Background, 44 | 45 | /// The color of the cursor. 46 | Cursor, 47 | 48 | /// The color used for text selection. 49 | Selection, 50 | 51 | /// The color for error markers. 52 | ErrorMarker, 53 | 54 | /// The color for breakpoints. 55 | Breakpoint, 56 | 57 | /// The color used for line numbers. 58 | LineNumber, 59 | 60 | /// The fill color for the current line. 61 | CurrentLineFill, 62 | 63 | /// The fill color for the current line when it is inactive. 64 | CurrentLineFillInactive, 65 | 66 | /// The edge color for the current line. 67 | CurrentLineEdge, 68 | 69 | /// The color used for the currently executing line when the editor is used in a debugger. 70 | ExecutingLine, 71 | 72 | /// This index and any values higher than it are for custom user-controlled colors 73 | Custom, 74 | } 75 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorErrorMarkers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ImGuiColorTextEditNet.Editor; 5 | 6 | /// Manages error markers. 7 | public class TextEditorErrorMarkers 8 | { 9 | Dictionary _errorMarkers = new(); 10 | 11 | /// Formatter for error markers, which converts the error context to a string representation. 12 | public Func ErrorMarkerFormatter { get; set; } = x => x.ToString() ?? ""; 13 | 14 | internal TextEditorErrorMarkers(TextEditorText text) 15 | { 16 | ArgumentNullException.ThrowIfNull(text); 17 | text.AllTextReplaced += () => _errorMarkers.Clear(); 18 | text.LineAdded += TextOnLineAdded; 19 | text.LinesRemoved += _text_LinesRemoved; 20 | } 21 | 22 | /// Retrieves the error marker for the specified line number (if any). 23 | public bool TryGetErrorForLine(int lineNo, out string errorInfo) 24 | { 25 | if (!_errorMarkers.TryGetValue(lineNo, out var error)) 26 | { 27 | errorInfo = ""; 28 | return false; 29 | } 30 | 31 | errorInfo = ErrorMarkerFormatter(error); 32 | return true; 33 | } 34 | 35 | /// Adds an error marker at the specified line number with the given context. 36 | public void Add(int lineNumber, int context) => _errorMarkers[lineNumber] = context; 37 | 38 | /// Sets the error markers, replacing any existing markers. 39 | public void SetErrorMarkers(Dictionary value) 40 | { 41 | _errorMarkers.Clear(); 42 | foreach (var kvp in value) 43 | _errorMarkers[kvp.Key] = kvp.Value; 44 | } 45 | 46 | internal object SerializeState() => _errorMarkers; 47 | 48 | void TextOnLineAdded(int index) 49 | { 50 | var tempErrors = new Dictionary(_errorMarkers.Count); 51 | foreach (var i in _errorMarkers) 52 | tempErrors[i.Key >= index ? i.Key + 1 : i.Key] = i.Value; 53 | _errorMarkers = tempErrors; 54 | } 55 | 56 | void _text_LinesRemoved(int start, int end) 57 | { 58 | var tempErrors = new Dictionary(); 59 | int lineCount = end - start + 1; 60 | foreach (var kvp in _errorMarkers) 61 | { 62 | int key = kvp.Key >= start ? kvp.Key - lineCount : kvp.Key; 63 | if (key >= start && key <= end) 64 | continue; 65 | 66 | tempErrors[key] = kvp.Value; 67 | } 68 | _errorMarkers = tempErrors; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorUndoStack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using ImGuiColorTextEditNet.Operations; 6 | 7 | namespace ImGuiColorTextEditNet.Editor; 8 | 9 | internal class TextEditorUndoStack 10 | { 11 | readonly TextEditorOptions _options; 12 | readonly TextEditorText _text; 13 | 14 | readonly List _undoBuffer = new(); 15 | int _undoIndex; 16 | 17 | internal TextEditorUndoStack(TextEditorText text, TextEditorOptions options) 18 | { 19 | _text = text ?? throw new ArgumentNullException(nameof(text)); 20 | _options = options ?? throw new ArgumentNullException(nameof(options)); 21 | _text.AllTextReplaced += Clear; 22 | } 23 | 24 | internal void Clear() 25 | { 26 | _undoBuffer.Clear(); 27 | _undoIndex = 0; 28 | } 29 | 30 | internal int UndoCount => _undoBuffer.Count; // Only for unit testing 31 | internal int UndoIndex => _undoIndex; // Only for unit testing 32 | 33 | internal bool CanUndo() => !_options.IsReadOnly && _undoIndex > 0; 34 | 35 | internal bool CanRedo() => !_options.IsReadOnly && _undoIndex < _undoBuffer.Count; 36 | 37 | internal void AddUndo(IEditorOperation operation) 38 | { 39 | Util.Assert(!_options.IsReadOnly); 40 | 41 | // If we are in the middle of the undo stack, remove all records after the current index 42 | if (_undoIndex < _undoBuffer.Count) 43 | _undoBuffer.RemoveRange(_undoIndex, _undoBuffer.Count - _undoIndex); 44 | 45 | _undoBuffer.Insert(_undoIndex, operation); 46 | ++_undoIndex; 47 | } 48 | 49 | internal void Undo(TextEditor editor, int aSteps = 1) 50 | { 51 | while (CanUndo() && aSteps-- > 0) 52 | { 53 | var operation = _undoBuffer[--_undoIndex]; 54 | operation.Undo(editor); 55 | } 56 | } 57 | 58 | internal void Redo(TextEditor editor, int aSteps = 1) 59 | { 60 | while (CanRedo() && aSteps-- > 0) 61 | { 62 | var operation = _undoBuffer[_undoIndex++]; 63 | operation.Apply(editor); 64 | } 65 | } 66 | 67 | public object SerializeState() 68 | { 69 | var state = new 70 | { 71 | UndoIndex = _undoIndex, 72 | UndoBuffer = _undoBuffer.Select(x => x.SerializeState()).ToList(), 73 | }; 74 | 75 | return JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }); 76 | } 77 | 78 | public void Do(IEditorOperation operation, TextEditor e) 79 | { 80 | operation.Apply(e); 81 | AddUndo(operation); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorColor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiColorTextEditNet.Syntax; 4 | 5 | namespace ImGuiColorTextEditNet.Editor; 6 | 7 | internal class TextEditorColor 8 | { 9 | internal ISyntaxHighlighter SyntaxHighlighter 10 | { 11 | get => _syntaxHighlighter; 12 | set 13 | { 14 | _syntaxHighlighter = value; 15 | InvalidateColor(0, -1); 16 | } 17 | } 18 | 19 | readonly TextEditorOptions _options; 20 | readonly TextEditorText _text; 21 | readonly List _lineState = new(); 22 | int _colorRangeMin; 23 | int _colorRangeMax; 24 | ISyntaxHighlighter _syntaxHighlighter = NullSyntaxHighlighter.Instance; 25 | 26 | internal TextEditorColor(TextEditorOptions options, TextEditorText text) 27 | { 28 | _options = options ?? throw new ArgumentNullException(nameof(options)); 29 | _text = text ?? throw new ArgumentNullException(nameof(text)); 30 | _text.AllTextReplaced += () => InvalidateColor(0, -1); 31 | } 32 | 33 | internal void ColorizeIncremental() 34 | { 35 | if ( 36 | _text.LineCount == 0 37 | || !_options.IsColorizerEnabled 38 | || _colorRangeMin >= _colorRangeMax 39 | ) 40 | { 41 | return; 42 | } 43 | 44 | int increment = SyntaxHighlighter.MaxLinesPerFrame; 45 | int to = Math.Min(_colorRangeMin + increment, _colorRangeMax); 46 | 47 | for (int lineIndex = _colorRangeMin; lineIndex < to; lineIndex++) 48 | { 49 | if (_lineState.Count <= lineIndex) 50 | _lineState.Add(null); 51 | 52 | var glyphs = _text.GetMutableLine(lineIndex); 53 | var state = lineIndex > 0 ? _lineState[lineIndex - 1] : null; 54 | state = SyntaxHighlighter.Colorize(glyphs, state); 55 | _lineState[lineIndex] = state; 56 | } 57 | 58 | _colorRangeMin = Math.Max(0, to); 59 | 60 | if (_colorRangeMax == _colorRangeMin) // Done? 61 | { 62 | _colorRangeMin = int.MaxValue; 63 | _colorRangeMax = 0; 64 | } 65 | } 66 | 67 | internal void InvalidateColor(int fromLine, int lineCount) // lineCount -1 = all lines after fromLine 68 | { 69 | fromLine = Math.Min(_colorRangeMin, fromLine); 70 | fromLine = Math.Max(0, fromLine); 71 | 72 | int toLine = _text.LineCount; 73 | 74 | if (lineCount != -1) 75 | toLine = Math.Min(_text.LineCount, fromLine + lineCount); 76 | 77 | toLine = Math.Max(_colorRangeMax, toLine); 78 | toLine = Math.Max(fromLine, toLine); 79 | 80 | _colorRangeMin = fromLine; 81 | _colorRangeMax = toLine; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorBreakpoints.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ImGuiColorTextEditNet.Editor; 5 | 6 | /// Manages breakpoints, allowing for setting, checking, and removing breakpoints on specific lines. 7 | public class TextEditorBreakpoints 8 | { 9 | Dictionary _breakpoints = new(); 10 | 11 | internal TextEditorBreakpoints(TextEditorText text) 12 | { 13 | ArgumentNullException.ThrowIfNull(text); 14 | text.AllTextReplaced += () => _breakpoints.Clear(); 15 | text.LineAdded += TextOnLineAdded; 16 | text.LinesRemoved += TextOnLinesRemoved; 17 | } 18 | 19 | /// Event that is raised when a breakpoint is removed. 20 | public event EventHandler? BreakpointRemoved; 21 | 22 | /// Checks if a breakpoint exists on the specified line number. 23 | public bool IsLineBreakpoint(int lineNumber) => _breakpoints.ContainsKey(lineNumber); 24 | 25 | /// Adds a breakpoint at the specified line number with the given context. 26 | public void Add(int lineNumber, object context) => _breakpoints[lineNumber] = context; 27 | 28 | /// Removes the breakpoint at the specified line number, if it exists. 29 | public void SetBreakpoints(IEnumerable<(int, object)> breakpoints) 30 | { 31 | _breakpoints.Clear(); 32 | foreach (var (line, context) in breakpoints) 33 | _breakpoints[line] = context; 34 | } 35 | 36 | /// Removes the breakpoint at the specified line number, if it exists. 37 | public object? GetBreakpoint(int lineNumber) 38 | { 39 | _breakpoints.TryGetValue(lineNumber, out var value); 40 | return value; 41 | } 42 | 43 | internal object SerializeState() => _breakpoints; 44 | 45 | void TextOnLineAdded(int index) 46 | { 47 | Dictionary newBreakpoints = new(); 48 | foreach (var kvp in _breakpoints) 49 | { 50 | int newIndex = kvp.Key >= index ? kvp.Key + 1 : kvp.Key; 51 | newBreakpoints[newIndex] = kvp.Value; 52 | } 53 | 54 | _breakpoints = newBreakpoints; 55 | } 56 | 57 | void TextOnLinesRemoved(int start, int end) 58 | { 59 | var newBreakpoints = new Dictionary(); 60 | int lineCount = end - start + 1; 61 | foreach (var kvp in _breakpoints) 62 | { 63 | var i = kvp.Key; 64 | if (i >= start && i <= end) 65 | { 66 | BreakpointRemoved?.Invoke(this, new(kvp.Value)); 67 | continue; 68 | } 69 | 70 | var newIndex = i >= start ? i - lineCount : i; 71 | newBreakpoints[newIndex] = kvp.Value; 72 | } 73 | 74 | _breakpoints = newBreakpoints; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/TextEdit/Line.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using ImGuiColorTextEditNet.Editor; 5 | 6 | namespace ImGuiColorTextEditNet; 7 | 8 | /// Represents a line of text. 9 | public class Line 10 | { 11 | /// 12 | /// A list of objects representing the characters in this line. 13 | /// 14 | public List Glyphs { get; init; } 15 | 16 | /// The length of the line (i.e., the number of glyphs in the line). 17 | public int Length => Glyphs.Count; 18 | 19 | /// Initializes a new instance of the class with an empty list of glyphs. 20 | public Line() => Glyphs = []; 21 | 22 | /// Initializes a new instance of the class with the specified list of glyphs. 23 | public Line(List glyphs) => 24 | Glyphs = glyphs ?? throw new ArgumentNullException(nameof(glyphs)); 25 | 26 | /// Appends a string to the line using the color of the last glyph, or the default color if there are no glyphs. 27 | public void Append(string s) 28 | { 29 | var color = Glyphs.Count > 0 ? Glyphs[^1].ColorIndex : PaletteIndex.Default; 30 | Append(color, s); 31 | } 32 | 33 | /// Appends a string to the line using the specified color index for all characters in the string. 34 | public void Append(PaletteIndex color, string s) 35 | { 36 | foreach (var c in s) 37 | Glyphs.Add(new Glyph(c, color)); 38 | } 39 | 40 | internal void GetString(StringBuilder sb, int start = 0) => GetString(sb, start, Glyphs.Count); 41 | 42 | internal void GetString(StringBuilder sb, int start, int end) 43 | { 44 | if (end > Glyphs.Count) 45 | end = Glyphs.Count; 46 | 47 | for (int i = start; i < end; i++) 48 | sb.Append(Glyphs[i].Char); 49 | } 50 | 51 | internal int GetCharacterIndex(Coordinates position, TextEditorOptions options) 52 | { 53 | int i = 0; 54 | for (int c = 0; i < Glyphs.Count && c < position.Column; ) 55 | { 56 | if (Glyphs[i].Char == '\t') 57 | c = options.NextTab(c); 58 | else 59 | c++; 60 | 61 | i++; 62 | } 63 | 64 | return i; 65 | } 66 | 67 | internal int GetCharacterColumn(int indexInLine, TextEditorOptions options) 68 | { 69 | int col = 0; 70 | 71 | int i = 0; 72 | while (i < indexInLine && i < Glyphs.Count) 73 | { 74 | if (Glyphs[i].Char == '\t') 75 | col = options.NextTab(col); 76 | else 77 | col++; 78 | i++; 79 | } 80 | 81 | return col; 82 | } 83 | 84 | internal int GetLineMaxColumn(TextEditorOptions options) => 85 | GetCharacterColumn(int.MaxValue, options); 86 | } 87 | -------------------------------------------------------------------------------- /src/TextEdit/Palettes.cs: -------------------------------------------------------------------------------- 1 | namespace ImGuiColorTextEditNet; 2 | 3 | /// 4 | /// Built-in color schemes 5 | /// 6 | public static class Palettes 7 | { 8 | /// Default dark theme 9 | public static readonly uint[] Dark = 10 | [ 11 | 0xff7f7f7f, // Default 12 | 0xffd69c56, // Keyword 13 | 0xff00ff00, // Number 14 | 0xff7070e0, // String 15 | 0xff70a0e0, // Char literal 16 | 0xffffffff, // Punctuation 17 | 0xff408080, // Preprocessor 18 | 0xffaaaaaa, // Identifier 19 | 0xff9bc64d, // Known identifier 20 | 0xffc040a0, // Preproc identifier 21 | 0xff50c050, // Comment (single line) 22 | 0xff70c050, // Comment (multi line) 23 | 0xff101010, // Background 24 | 0xffe0e0e0, // Cursor 25 | 0x80a06020, // Selection 26 | 0x800020ff, // ErrorMarker 27 | 0x40f08000, // Breakpoint 28 | 0xff707000, // Line number 29 | 0x40000000, // Current line fill 30 | 0x40808080, // Current line fill (inactive) 31 | 0x40a0a0a0, // Current line edge 32 | 0xa0a0a0a0, // Executing Line 33 | ]; 34 | 35 | /// Default light theme 36 | public static readonly uint[] Light = 37 | [ 38 | 0xff7f7f7f, // None 39 | 0xffff0c06, // Keyword 40 | 0xff008000, // Number 41 | 0xff2020a0, // String 42 | 0xff304070, // Char literal 43 | 0xff000000, // Punctuation 44 | 0xff406060, // Preprocessor 45 | 0xff404040, // Identifier 46 | 0xff606010, // Known identifier 47 | 0xffc040a0, // Preproc identifier 48 | 0xff205020, // Comment (single line) 49 | 0xff405020, // Comment (multi line) 50 | 0xffffffff, // Background 51 | 0xff000000, // Cursor 52 | 0x80600000, // Selection 53 | 0xa00010ff, // ErrorMarker 54 | 0x80f08000, // Breakpoint 55 | 0xff505000, // Line number 56 | 0x40000000, // Current line fill 57 | 0x40808080, // Current line fill (inactive) 58 | 0x40000000, // Current line edge 59 | 0xa0a0a0a0, // Executing Line 60 | ]; 61 | 62 | /// Default blue theme 63 | public static readonly uint[] RetroBlue = 64 | [ 65 | 0xff00ffff, // None 66 | 0xffffff00, // Keyword 67 | 0xff00ff00, // Number 68 | 0xff808000, // String 69 | 0xff808000, // Char literal 70 | 0xffffffff, // Punctuation 71 | 0xff008000, // Preprocessor 72 | 0xff00ffff, // Identifier 73 | 0xffffffff, // Known identifier 74 | 0xffff00ff, // Preproc identifier 75 | 0xff808080, // Comment (single line) 76 | 0xff404040, // Comment (multi line) 77 | 0xff800000, // Background 78 | 0xff0080ff, // Cursor 79 | 0x80ffff00, // Selection 80 | 0xa00000ff, // ErrorMarker 81 | 0x80ff8000, // Breakpoint 82 | 0xff808000, // Line number 83 | 0x40000000, // Current line fill 84 | 0x40808080, // Current line fill (inactive) 85 | 0x40000000, // Current line edge 86 | 0xa0a0a0a0, // Executing Line 87 | ]; 88 | } 89 | -------------------------------------------------------------------------------- /src/TextEdit/Input/StandardMouseInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImGuiNET; 3 | 4 | namespace ImGuiColorTextEditNet.Input; 5 | 6 | /// Represents the standard mouse input handling. 7 | public class StandardMouseInput : ITextEditorMouseInput 8 | { 9 | readonly TextEditor _editor; 10 | 11 | /// Initializes a new instance of the class. 12 | public StandardMouseInput(TextEditor editor) => 13 | _editor = editor ?? throw new ArgumentNullException(nameof(editor)); 14 | 15 | /// Handles mouse inputs for the text editor. 16 | public void HandleMouseInputs() 17 | { 18 | var io = ImGui.GetIO(); 19 | var shift = io.KeyShift; 20 | var ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; 21 | var alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; 22 | 23 | if (!ImGui.IsWindowHovered()) 24 | return; 25 | 26 | ImGui.SetMouseCursor(ImGuiMouseCursor.TextInput); 27 | 28 | if (shift || alt) 29 | return; 30 | 31 | var click = ImGui.IsMouseClicked(0); 32 | var doubleClick = ImGui.IsMouseDoubleClicked(0); 33 | 34 | // Left mouse button double click 35 | if (doubleClick) 36 | { 37 | if (!ctrl) 38 | { 39 | _editor.Selection.Cursor = 40 | _editor.Selection.InteractiveStart = 41 | _editor.Selection.InteractiveEnd = 42 | _editor.Renderer.ScreenPosToCoordinates(ImGui.GetMousePos()); 43 | 44 | _editor.Selection.Mode = 45 | _editor.Selection.Mode == SelectionMode.Line 46 | ? SelectionMode.Normal 47 | : SelectionMode.Word; 48 | 49 | _editor.Selection.Select( 50 | _editor.Selection.InteractiveStart, 51 | _editor.Selection.InteractiveEnd, 52 | _editor.Selection.Mode 53 | ); 54 | } 55 | } 56 | else if (click) // Left mouse button click 57 | { 58 | _editor.CursorPosition = 59 | _editor.Selection.InteractiveStart = 60 | _editor.Selection.InteractiveEnd = 61 | _editor.Renderer.ScreenPosToCoordinates(ImGui.GetMousePos()); 62 | 63 | _editor.Selection.Mode = ctrl ? SelectionMode.Word : SelectionMode.Normal; 64 | 65 | _editor.Selection.Select( 66 | _editor.Selection.InteractiveStart, 67 | _editor.Selection.InteractiveEnd, 68 | _editor.Selection.Mode 69 | ); 70 | } 71 | else if (ImGui.IsMouseDragging(0) && ImGui.IsMouseDown(0)) // Mouse left button dragging (=> update selection) 72 | { 73 | io.WantCaptureMouse = true; 74 | _editor.Selection.Cursor = _editor.Selection.InteractiveEnd = 75 | _editor.Renderer.ScreenPosToCoordinates(ImGui.GetMousePos()); 76 | 77 | _editor.Selection.Select( 78 | _editor.Selection.InteractiveStart, 79 | _editor.Selection.InteractiveEnd, 80 | _editor.Selection.Mode 81 | ); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/TextEdit/Operations/UndoRecord.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace ImGuiColorTextEditNet.Operations; 4 | 5 | internal class UndoRecord : IEditorOperation 6 | { 7 | public string? Added; 8 | public Coordinates AddedStart; 9 | public Coordinates AddedEnd; 10 | 11 | public string? Removed; 12 | public Coordinates RemovedStart; 13 | public Coordinates RemovedEnd; 14 | 15 | public SelectionState Before; 16 | public SelectionState After; 17 | 18 | public override string ToString() 19 | { 20 | var sb = new StringBuilder(); 21 | if (Added != null) 22 | { 23 | sb.Append("+\""); 24 | sb.Append(Added); 25 | sb.Append("\" @ "); 26 | sb.Append(AddedStart); 27 | } 28 | 29 | if (Removed != null) 30 | { 31 | if (sb.Length > 0) 32 | sb.Append(' '); 33 | sb.Append("-\""); 34 | sb.Append(Removed); 35 | sb.Append("\" @ "); 36 | sb.Append(RemovedStart); 37 | } 38 | 39 | sb.Append(' '); 40 | sb.Append(Before); 41 | sb.Append(" => "); 42 | sb.Append(After); 43 | 44 | return sb.ToString(); 45 | } 46 | 47 | public object SerializeState() => 48 | new 49 | { 50 | Added, 51 | AddedStart = AddedStart.ToString(), 52 | AddedEnd = AddedEnd.ToString(), 53 | 54 | Removed, 55 | RemovedStart = RemovedStart.ToString(), 56 | RemovedEnd = RemovedEnd.ToString(), 57 | 58 | Before = Before.ToString(), 59 | After = After.ToString(), 60 | }; 61 | 62 | public void Apply(TextEditor editor) 63 | { 64 | if (!string.IsNullOrEmpty(Removed)) 65 | { 66 | editor.Text.DeleteRange(RemovedStart, RemovedEnd); 67 | editor.Color.InvalidateColor( 68 | RemovedStart.Line - 1, 69 | RemovedEnd.Line - RemovedStart.Line + 1 70 | ); 71 | } 72 | 73 | if (!string.IsNullOrEmpty(Added)) 74 | { 75 | var start = AddedStart; 76 | editor.Text.InsertTextAt(start, Added); 77 | editor.Color.InvalidateColor(AddedStart.Line - 1, AddedEnd.Line - AddedStart.Line + 1); 78 | } 79 | 80 | editor.Selection.Select(After.Start, After.End); 81 | editor.Selection.Cursor = After.Cursor; 82 | editor.Text.PendingScrollRequest = editor.Selection.Cursor.Line; 83 | } 84 | 85 | public void Undo(TextEditor editor) 86 | { 87 | if (!string.IsNullOrEmpty(Added)) 88 | { 89 | editor.Text.DeleteRange(AddedStart, AddedEnd); 90 | editor.Color.InvalidateColor(AddedStart.Line - 1, AddedEnd.Line - AddedStart.Line + 2); 91 | } 92 | 93 | if (!string.IsNullOrEmpty(Removed)) 94 | { 95 | var start = RemovedStart; 96 | editor.Text.InsertTextAt(start, Removed); 97 | 98 | editor.Color.InvalidateColor( 99 | RemovedStart.Line - 1, 100 | RemovedEnd.Line - RemovedStart.Line + 2 101 | ); 102 | } 103 | 104 | editor.Selection.Select(Before.Start, Before.End); 105 | editor.Selection.Cursor = Before.Cursor; 106 | editor.Text.PendingScrollRequest = editor.Selection.Cursor.Line; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/TextEdit/SimpleTrie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | 5 | namespace ImGuiColorTextEditNet; 6 | 7 | internal class SimpleTrie 8 | where TInfo : class 9 | { 10 | class Node 11 | { 12 | public readonly string Path; 13 | public TInfo? Info; 14 | public readonly Dictionary Children = new(); 15 | 16 | public Node(string path) => Path = path; 17 | 18 | public override string ToString() => $"{Path} ({Children.Count}): {Info}"; 19 | } 20 | 21 | readonly Node _root = new(""); 22 | 23 | /// 24 | /// Adds an entry to the trie 25 | /// 26 | /// The name to use for looking up the entry 27 | /// The entry data 28 | /// true if entry added, false if an entry already existed for the given name 29 | public bool Add(string name, TInfo info) 30 | { 31 | var node = _root; 32 | foreach (var c in name) 33 | { 34 | if (!node.Children.TryGetValue(c, out var newNode)) 35 | { 36 | newNode = new(node.Path + c); 37 | node.Children[c] = newNode; 38 | } 39 | 40 | node = newNode; 41 | } 42 | 43 | if (node.Info != null) 44 | return false; 45 | 46 | node.Info = info; 47 | return true; 48 | } 49 | 50 | public TInfo? Get(ReadOnlySpan key, Func toChar) 51 | { 52 | var node = _root; 53 | foreach (var c in key) 54 | { 55 | if (node.Children.TryGetValue(toChar(c), out var newNode)) 56 | node = newNode; 57 | else 58 | return null; 59 | } 60 | 61 | return node.Info; 62 | } 63 | 64 | public TInfo? Get(ReadOnlySpan key) 65 | { 66 | var node = _root; 67 | foreach (var c in key) 68 | { 69 | if (node.Children.TryGetValue(c, out var newNode)) 70 | node = newNode; 71 | else 72 | return null; 73 | } 74 | 75 | return node.Info; 76 | } 77 | 78 | public bool Remove(string name) 79 | { 80 | if (string.IsNullOrEmpty(name)) 81 | return false; 82 | 83 | var node = _root; 84 | var pool = ArrayPool.Shared; 85 | var nodes = pool.Rent(name.Length + 1); 86 | nodes[0] = _root; 87 | 88 | for (int index = 0; index < name.Length; index++) 89 | { 90 | var c = name[index]; 91 | if (!node.Children.TryGetValue(c, out var newNode)) 92 | return false; 93 | 94 | nodes[index + 1] = node; 95 | node = newNode; 96 | } 97 | 98 | if (node.Info == null) 99 | { 100 | pool.Return(nodes); 101 | return false; 102 | } 103 | 104 | node.Info = null; 105 | 106 | for (int index = name.Length - 1; index >= 0; index--) 107 | { 108 | node = nodes[index + 1]; 109 | if (node.Info != null) 110 | break; 111 | 112 | if (node.Children.Count == 0) 113 | nodes[index].Children.Remove(name[index]); 114 | } 115 | 116 | pool.Return(nodes); 117 | return true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImGuiColorTextEditNet; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace TextEdit.Tests; 6 | 7 | [TestClass] 8 | public class BasicTests 9 | { 10 | [TestMethod] 11 | public void ConstructorTest() 12 | { 13 | var t = new TextEditor(); 14 | Assert.AreEqual("", t.AllText); 15 | Assert.AreEqual(1, t.TotalLines); 16 | 17 | var lines = t.TextLines; 18 | Assert.AreEqual(1, lines.Count); 19 | Assert.AreEqual("", lines[0]); 20 | } 21 | 22 | [TestMethod] 23 | public void SetTextTest() 24 | { 25 | var t = new TextEditor { AllText = "abc" }; 26 | 27 | Assert.AreEqual("abc", t.AllText); 28 | Assert.AreEqual(1, t.TotalLines); 29 | 30 | var lines = t.TextLines; 31 | Assert.AreEqual(1, lines.Count); 32 | Assert.AreEqual("abc", lines[0]); 33 | } 34 | 35 | [TestMethod] 36 | public void MultiLineSetTextTest() 37 | { 38 | const string text = 39 | @"abc 40 | def"; 41 | var t = new TextEditor { AllText = text }; 42 | Assert.AreEqual(text, t.AllText); 43 | Assert.AreEqual(2, t.TotalLines); 44 | 45 | var lines = t.TextLines; 46 | Assert.AreEqual(2, lines.Count); 47 | Assert.AreEqual("abc", lines[0]); 48 | Assert.AreEqual("def", lines[1]); 49 | } 50 | 51 | [TestMethod] 52 | public void TrailingNewlineSetTextTest() 53 | { 54 | const string text = 55 | @"abc 56 | "; 57 | var t = new TextEditor { AllText = text }; 58 | Assert.AreEqual(text, t.AllText); 59 | Assert.AreEqual(2, t.TotalLines); 60 | 61 | var lines = t.TextLines; 62 | Assert.AreEqual(2, lines.Count); 63 | Assert.AreEqual("abc", lines[0]); 64 | Assert.AreEqual("", lines[1]); 65 | } 66 | 67 | [TestMethod] 68 | public void SetTextEmptyTest() 69 | { 70 | var t = new TextEditor { AllText = "" }; 71 | Assert.AreEqual("", t.AllText); 72 | Assert.AreEqual(1, t.TotalLines); 73 | 74 | var lines = t.TextLines; 75 | Assert.AreEqual(1, lines.Count); 76 | Assert.AreEqual("", lines[0]); 77 | } 78 | 79 | [TestMethod] 80 | public void SetTextLinesTest() 81 | { 82 | var t = new TextEditor { TextLines = ["abc"] }; 83 | 84 | Assert.AreEqual("abc", t.AllText); 85 | Assert.AreEqual(1, t.TotalLines); 86 | 87 | var lines = t.TextLines; 88 | Assert.AreEqual(1, lines.Count); 89 | Assert.AreEqual("abc", lines[0]); 90 | } 91 | 92 | [TestMethod] 93 | public void MultiLineSetTextLinesTest() 94 | { 95 | const string text = 96 | @"abc 97 | def"; 98 | var t = new TextEditor { TextLines = ["abc", "def"] }; 99 | 100 | Assert.AreEqual(text, t.AllText); 101 | Assert.AreEqual(2, t.TotalLines); 102 | 103 | var lines = t.TextLines; 104 | Assert.AreEqual(2, lines.Count); 105 | Assert.AreEqual("abc", lines[0]); 106 | Assert.AreEqual("def", lines[1]); 107 | } 108 | 109 | [TestMethod] 110 | public void SetTextLinesEmptyTest() 111 | { 112 | var t = new TextEditor { TextLines = Array.Empty() }; 113 | Assert.AreEqual("", t.AllText); 114 | Assert.AreEqual(1, t.TotalLines); 115 | 116 | var lines = t.TextLines; 117 | Assert.AreEqual(1, lines.Count); 118 | Assert.AreEqual("", lines[0]); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorSelection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Editor; 4 | 5 | /// Handles text selection, allowing for selecting text ranges, words, or lines. 6 | public class TextEditorSelection 7 | { 8 | readonly TextEditorText _text; 9 | SelectionState _state; 10 | 11 | internal SelectionState State => _state; 12 | internal SelectionMode Mode = SelectionMode.Normal; 13 | internal Coordinates InteractiveStart; 14 | internal Coordinates InteractiveEnd; 15 | 16 | internal TextEditorSelection(TextEditorText text) 17 | { 18 | _text = text ?? throw new ArgumentNullException(nameof(text)); 19 | } 20 | 21 | /// Gets the currently selected text. 22 | public string GetSelectedText() => _text.GetText(_state.Start, _state.End); 23 | 24 | internal Coordinates GetActualCursorCoordinates() => _text.SanitizeCoordinates(Cursor); 25 | 26 | /// Gets or sets the line number that is highlighted (if any). 27 | public int? HighlightedLine { get; set; } 28 | 29 | /// Gets or sets the current cursor position. 30 | public Coordinates Cursor 31 | { 32 | get => _state.Cursor; 33 | internal set => _state.Cursor = value; 34 | } 35 | 36 | /// Gets or sets the start coordinates of the selection. 37 | public Coordinates Start 38 | { 39 | get => _state.Start; 40 | set 41 | { 42 | _state.Start = _text.SanitizeCoordinates(value); 43 | if (_state.Start > _state.End) 44 | (_state.Start, _state.End) = (_state.End, _state.Start); 45 | } 46 | } 47 | 48 | /// Gets or sets the end coordinates of the selection. 49 | public Coordinates End 50 | { 51 | get => _state.End; 52 | set 53 | { 54 | _state.End = _text.SanitizeCoordinates(value); 55 | if (_state.Start > _state.End) 56 | (_state.Start, _state.End) = (_state.End, _state.Start); 57 | } 58 | } 59 | 60 | internal object SerializeState() => 61 | new 62 | { 63 | Cursor = Cursor.ToString(), 64 | Start = Start.ToString(), 65 | End = End.ToString(), 66 | Mode, 67 | }; 68 | 69 | /// Selects the word that is currently under the cursor. 70 | public void SelectWordUnderCursor() => 71 | Select(_text.FindWordStart(Cursor), _text.FindWordEnd(Cursor)); 72 | 73 | /// Selects all text. 74 | public void SelectAll() => Select((0, 0), (_text.LineCount, 0)); 75 | 76 | /// Indicates whether there is an active selection. 77 | public bool HasSelection => End > Start; 78 | 79 | /// Selects a range of text based on the specified start and end coordinates. 80 | public void Select( 81 | Coordinates start, 82 | Coordinates end, 83 | SelectionMode mode = SelectionMode.Normal 84 | ) 85 | { 86 | _state.Start = _text.SanitizeCoordinates(start); 87 | End = _text.SanitizeCoordinates(end); 88 | 89 | switch (mode) 90 | { 91 | case SelectionMode.Normal: 92 | break; 93 | 94 | case SelectionMode.Word: 95 | { 96 | Start = _text.FindWordStart(Start); 97 | if (!_text.IsOnWordBoundary(End)) 98 | End = _text.FindWordEnd(_text.FindWordStart(End)); 99 | break; 100 | } 101 | 102 | case SelectionMode.Line: 103 | { 104 | Start = (Start.Line, 0); 105 | End = (End.Line, _text.GetLineMaxColumn(End.Line)); 106 | break; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/TextEdit/Coordinates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet; 4 | 5 | /// 6 | /// Represents a character coordinate from the user's point of view, 7 | /// i.e. consider a uniform grid (assuming fixed-width font) on the 8 | /// screen as it is rendered, and each cell has its own coordinate, starting from 0. 9 | /// Tabs are counted as between 1 and mTabSize empty spaces, depending on 10 | /// how many spaces are necessary to reach the next tab stop. 11 | /// For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, 12 | /// because it is rendered as " ABC" on the screen. 13 | /// 14 | public struct Coordinates : IEquatable 15 | { 16 | /// The line number, starting from 0. 17 | public int Line; 18 | 19 | /// The column number, starting from 0. 20 | public int Column; 21 | 22 | /// Creates a new instance of Coordinates at (0,0) 23 | public Coordinates() 24 | { 25 | Line = 0; 26 | Column = 0; 27 | } 28 | 29 | /// Creates a new instance of Coordinates at the specified line and column. 30 | public Coordinates(int line, int column) 31 | { 32 | Util.Assert(line >= 0); 33 | Util.Assert(column >= 0); 34 | Line = line; 35 | Column = column; 36 | } 37 | 38 | /// Implicitly converts a tuple of (line, column) to Coordinates. 39 | public static implicit operator Coordinates((int Line, int Column) x) => new(x.Line, x.Column); 40 | 41 | /// 42 | /// Returns a string representation of the coordinates in the format "line:column". 43 | /// 44 | public override string ToString() => $"{Line}:{Column}"; 45 | 46 | /// Represents an invalid coordinate, which is used to indicate that a coordinate is not valid or has not been set. 47 | public static Coordinates Invalid => new() { Line = -1, Column = -1 }; 48 | 49 | /// Compares two Coordinates for equality. 50 | public static bool operator ==(Coordinates x, Coordinates y) => 51 | x.Line == y.Line && x.Column == y.Column; 52 | 53 | /// Compares two Coordinates for inequality. 54 | public static bool operator !=(Coordinates x, Coordinates y) => 55 | x.Line != y.Line || x.Column != y.Column; 56 | 57 | /// Compares two Coordinates to determine if one is less than the other. 58 | public static bool operator <(Coordinates x, Coordinates y) => 59 | x.Line != y.Line ? x.Line < y.Line : x.Column < y.Column; 60 | 61 | /// Compares two Coordinates to determine if one is greater than the other. 62 | public static bool operator >(Coordinates x, Coordinates y) => 63 | x.Line != y.Line ? x.Line > y.Line : x.Column > y.Column; 64 | 65 | /// Compares two Coordinates to determine if one is less than or equal to the other. 66 | public static bool operator <=(Coordinates x, Coordinates y) => 67 | x.Line != y.Line ? x.Line < y.Line : x.Column <= y.Column; 68 | 69 | /// Compares two Coordinates to determine if one is greater than or equal to the other. 70 | public static bool operator >=(Coordinates x, Coordinates y) => 71 | x.Line != y.Line ? x.Line > y.Line : x.Column >= y.Column; 72 | 73 | /// Adds two coordinates 74 | public static Coordinates operator +(Coordinates x, Coordinates y) => 75 | new(x.Line + y.Line, x.Column + y.Column); 76 | 77 | /// Subtracts two coordinates 78 | public static Coordinates operator -(Coordinates x, Coordinates y) => 79 | new(x.Line - y.Line, x.Column - y.Column); 80 | 81 | /// Checks if the current Coordinates instance is equal to another Coordinates instance. 82 | public bool Equals(Coordinates other) => Line == other.Line && Column == other.Column; 83 | 84 | /// Checks if the current Coordinates instance is equal to another object. 85 | public override bool Equals(object? obj) => obj is Coordinates other && Equals(other); 86 | 87 | /// Returns a hash code for the current Coordinates instance based on its Line and Column values. 88 | public override int GetHashCode() => HashCode.Combine(Line, Column); 89 | } 90 | -------------------------------------------------------------------------------- /src/TextEdit.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using ImGuiColorTextEditNet; 3 | using ImGuiColorTextEditNet.Syntax; 4 | using ImGuiNET; 5 | using Veldrid; 6 | using Veldrid.Sdl2; 7 | using Veldrid.StartupUtilities; 8 | 9 | namespace TextEdit.Demo; 10 | 11 | public static class Program 12 | { 13 | public static void Main() 14 | { 15 | var (window, gd) = CreateWindow(); 16 | using var controller = new ImGuiController( 17 | gd, 18 | gd.MainSwapchain.Framebuffer.OutputDescription, 19 | window.Width, 20 | window.Height 21 | ); 22 | 23 | var font = SetupFont(controller); 24 | var cl = gd.ResourceFactory.CreateCommandList(); 25 | 26 | window.Resized += () => 27 | { 28 | gd.ResizeMainWindow((uint)window.Width, (uint)window.Height); 29 | controller.WindowResized(window.Width, window.Height); 30 | }; 31 | 32 | const string demoText = """ 33 | #include 34 | 35 | void main(int argc, char **argv) { 36 | printf("Hello world!\n"); 37 | /* A multi-line 38 | comment which continues on 39 | to here */ 40 | 41 | for (int i = 0; i < 10; i++) 42 | printf("%d\n", i); // Breakpoint here 43 | 44 | // A single line comment 45 | int a = 123456; 46 | int b = 0x123456; // and here 47 | int c = 0b110101; 48 | errors on this line! 49 | } 50 | """; 51 | 52 | var editor = new TextEditor 53 | { 54 | AllText = demoText, 55 | SyntaxHighlighter = new CStyleHighlighter(true), 56 | }; 57 | 58 | (int, object)[] demoBreakpoints = [(10, ""), (14, "")]; 59 | var demoErrors = new Dictionary { { 16, "Syntax error etc" } }; 60 | editor.Breakpoints.SetBreakpoints(demoBreakpoints); 61 | editor.ErrorMarkers.SetErrorMarkers(demoErrors); 62 | 63 | editor.SetColor(PaletteIndex.Custom, 0xff0000ff); 64 | editor.SetColor(PaletteIndex.Custom + 1, 0xff00ffff); 65 | editor.SetColor(PaletteIndex.Custom + 2, 0xffffffff); 66 | editor.SetColor(PaletteIndex.Custom + 3, 0xff808080); 67 | 68 | DateTime lastFrame = DateTime.Now; 69 | 70 | while (window.Exists) 71 | { 72 | var input = window.PumpEvents(); 73 | if (!window.Exists) 74 | break; 75 | 76 | var thisFrame = DateTime.Now; 77 | controller.Update((float)(thisFrame - lastFrame).TotalSeconds, input); 78 | lastFrame = thisFrame; 79 | 80 | ImGui.SetNextWindowPos(new Vector2(0, 0)); 81 | ImGui.SetNextWindowSize(new Vector2(window.Width, window.Height)); 82 | ImGui.PushFont(font); 83 | ImGui.Begin("Demo"); 84 | 85 | if (ImGui.Button("Reset")) 86 | { 87 | editor.AllText = demoText; 88 | editor.Breakpoints.SetBreakpoints(demoBreakpoints); 89 | editor.ErrorMarkers.SetErrorMarkers(demoErrors); 90 | } 91 | 92 | ImGui.SameLine(); 93 | if (ImGui.Button("err line")) 94 | editor.AppendLine("Some error text", PaletteIndex.Custom); 95 | 96 | ImGui.SameLine(); 97 | if (ImGui.Button("warn line")) 98 | editor.AppendLine("Some warning text", PaletteIndex.Custom + 1); 99 | 100 | ImGui.SameLine(); 101 | if (ImGui.Button("info line")) 102 | editor.AppendLine("Some info text", PaletteIndex.Custom + 2); 103 | 104 | ImGui.SameLine(); 105 | if (ImGui.Button("verbose line")) 106 | editor.AppendLine("Some debug text", PaletteIndex.Custom + 3); 107 | 108 | ImGui.Text( 109 | $"Cur:{editor.CursorPosition} SEL: {editor.Selection.Start} - {editor.Selection.End}" 110 | ); 111 | editor.Render("EditWindow"); 112 | 113 | ImGui.End(); 114 | ImGui.PopFont(); 115 | 116 | cl.Begin(); 117 | cl.SetFramebuffer(gd.MainSwapchain.Framebuffer); 118 | cl.ClearColorTarget(0, RgbaFloat.Black); 119 | controller.Render(gd, cl); 120 | cl.End(); 121 | gd.SubmitCommands(cl); 122 | gd.SwapBuffers(gd.MainSwapchain); 123 | } 124 | } 125 | 126 | static (Sdl2Window, GraphicsDevice) CreateWindow() 127 | { 128 | var windowInfo = new WindowCreateInfo 129 | { 130 | X = 100, 131 | Y = 100, 132 | WindowWidth = 960, 133 | WindowHeight = 960, 134 | WindowInitialState = WindowState.Normal, 135 | WindowTitle = "TextEdit.Test", 136 | }; 137 | 138 | var gdOptions = new GraphicsDeviceOptions( 139 | true, 140 | null, 141 | true, 142 | ResourceBindingModel.Improved, 143 | true, 144 | true, 145 | false 146 | ); 147 | 148 | VeldridStartup.CreateWindowAndGraphicsDevice( 149 | windowInfo, 150 | gdOptions, 151 | out Sdl2Window? window, 152 | out GraphicsDevice? gd 153 | ); 154 | 155 | return (window, gd); 156 | } 157 | 158 | static unsafe ImFontPtr SetupFont(ImGuiController controller) 159 | { 160 | var io = ImGui.GetIO(); 161 | var nativeConfig = ImGuiNative.ImFontConfig_ImFontConfig(); 162 | nativeConfig->OversampleH = 8; 163 | nativeConfig->OversampleV = 8; 164 | nativeConfig->RasterizerMultiply = 1f; 165 | nativeConfig->GlyphOffset = new Vector2(0); 166 | 167 | var dir = Directory.GetCurrentDirectory(); 168 | var fontPath = Path.Combine(dir, "SpaceMono-Regular.ttf"); 169 | 170 | if (!File.Exists(fontPath)) 171 | throw new FileNotFoundException("Could not find font file at " + fontPath); 172 | 173 | var font = io.Fonts.AddFontFromFileTTF( 174 | fontPath, 175 | 16, // size in pixels 176 | nativeConfig 177 | ); 178 | 179 | if (font.NativePtr == (ImFont*)0) 180 | throw new InvalidOperationException("Font could not be loaded"); 181 | 182 | controller.RecreateFontDeviceTexture(); 183 | 184 | io.FontGlobalScale = 2.0f; 185 | return font; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/DeletionTests.cs: -------------------------------------------------------------------------------- 1 | using ImGuiColorTextEditNet; 2 | using ImGuiColorTextEditNet.Editor; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace TextEdit.Tests; 6 | 7 | [TestClass] 8 | public class DeletionTests 9 | { 10 | [TestMethod] 11 | public void BackspaceTest() => BackspaceTestInner(false, false); 12 | 13 | [TestMethod] 14 | public void BackspaceTestBp() => BackspaceTestInner(true, false); 15 | 16 | [TestMethod] 17 | public void BackspaceTestErr() => BackspaceTestInner(false, true); 18 | 19 | static void BackspaceTestInner(bool breakpoints, bool errors) 20 | { 21 | var t = new TextEditor { AllText = "abc" }; 22 | if (breakpoints) 23 | t.Breakpoints.Add(0, 1); 24 | if (errors) 25 | t.ErrorMarkers.Add(0, 1); 26 | Assert.AreEqual((0, 0), t.CursorPosition); 27 | 28 | UndoHelper.TestNopUndo(t, TextEditorModify.Backspace); // Backspace at start of a line should do nothing 29 | Assert.AreEqual("abc", t.AllText); 30 | Assert.AreEqual(0, t.UndoCount); 31 | Assert.AreEqual(0, t.UndoIndex); 32 | 33 | t.Selection.Select((0, 0), (0, 1)); 34 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 35 | Assert.AreEqual("bc", t.AllText); 36 | Assert.AreEqual(1, t.UndoCount); 37 | Assert.AreEqual(1, t.UndoIndex); 38 | 39 | t.CursorPosition = (0, 1); 40 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 41 | Assert.AreEqual("c", t.AllText); 42 | Assert.AreEqual(2, t.UndoCount); 43 | Assert.AreEqual(2, t.UndoIndex); 44 | } 45 | 46 | [TestMethod] 47 | public void BackspaceTestEol() 48 | { 49 | var t = new TextEditor { AllText = "abc" }; 50 | t.CursorPosition = (0, 3); 51 | 52 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 53 | Assert.AreEqual("ab", t.AllText); 54 | Assert.AreEqual(1, t.UndoCount); 55 | Assert.AreEqual(1, t.UndoIndex); 56 | } 57 | 58 | [TestMethod] 59 | public void BackspaceTestMultiLine1() => BackspaceTestMultiLine1Inner(false, false); 60 | 61 | static void BackspaceTestMultiLine1Inner(bool breakpoints, bool errors) 62 | { 63 | var before = 64 | @"one 65 | two 66 | three"; 67 | 68 | var after = @"onhree"; 69 | 70 | var t = new TextEditor { AllText = before }; 71 | if (breakpoints) 72 | t.Breakpoints.Add(0, 1); 73 | 74 | if (errors) 75 | t.ErrorMarkers.Add(0, 1); 76 | 77 | t.CursorPosition = (2, 1); 78 | t.Selection.Select((0, 2), (2, 1)); 79 | 80 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 81 | Assert.AreEqual(after, t.AllText); 82 | Assert.AreEqual((0, 2), t.CursorPosition); 83 | Assert.AreEqual((0, 2), t.Selection.Start); 84 | Assert.AreEqual((0, 2), t.Selection.End); 85 | Assert.AreEqual(1, t.UndoCount); 86 | Assert.AreEqual(1, t.UndoIndex); 87 | } 88 | 89 | [TestMethod] 90 | public void BackspaceTestMultiLine2() => BackspaceTestMultiLine2Inner(false, false); 91 | 92 | static void BackspaceTestMultiLine2Inner(bool breakpoints, bool errors) 93 | { 94 | var before = 95 | @"one 96 | two 97 | three"; 98 | 99 | var after = @"onhree"; 100 | 101 | var t = new TextEditor { AllText = before }; 102 | if (breakpoints) 103 | t.Breakpoints.Add(0, 1); 104 | if (errors) 105 | t.ErrorMarkers.Add(0, 1); 106 | 107 | t.CursorPosition = (0, 2); 108 | t.Selection.Select((0, 2), (2, 1)); 109 | 110 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 111 | Assert.AreEqual(after, t.AllText); 112 | Assert.AreEqual((0, 2), t.CursorPosition); 113 | Assert.AreEqual((0, 2), t.Selection.Start); 114 | Assert.AreEqual((0, 2), t.Selection.End); 115 | Assert.AreEqual(1, t.UndoCount); 116 | Assert.AreEqual(1, t.UndoIndex); 117 | } 118 | 119 | [TestMethod] 120 | public void BackspaceTestMultiLine3() => BackspaceTestMultiLine3Inner(false, false); 121 | 122 | static void BackspaceTestMultiLine3Inner(bool breakpoints, bool errors) 123 | { 124 | var before = 125 | @"first 126 | second 127 | third"; 128 | 129 | var after = 130 | @"firstsecond 131 | third"; 132 | 133 | var t = new TextEditor { AllText = before }; 134 | if (breakpoints) 135 | t.Breakpoints.Add(0, 1); 136 | if (errors) 137 | t.ErrorMarkers.Add(0, 1); 138 | 139 | t.CursorPosition = (1, 0); 140 | 141 | UndoHelper.TestUndo(t, TextEditorModify.Backspace); 142 | Assert.AreEqual(after, t.AllText); 143 | Assert.AreEqual((0, 5), t.CursorPosition); 144 | Assert.AreEqual((0, 5), t.Selection.Start); 145 | Assert.AreEqual((0, 5), t.Selection.End); 146 | Assert.AreEqual(1, t.UndoCount); 147 | Assert.AreEqual(1, t.UndoIndex); 148 | } 149 | 150 | [TestMethod] 151 | public void DeleteTest() => DeleteTestInner(false, false); 152 | 153 | static void DeleteTestInner(bool breakpoints, bool errors) 154 | { 155 | var t = new TextEditor { AllText = "abc" }; 156 | if (breakpoints) 157 | t.Breakpoints.Add(0, 1); 158 | if (errors) 159 | t.ErrorMarkers.Add(0, 1); 160 | 161 | Assert.AreEqual((0, 0), t.CursorPosition); 162 | 163 | UndoHelper.TestUndo(t, TextEditorModify.Delete); 164 | Assert.AreEqual("bc", t.AllText); 165 | Assert.AreEqual((0, 0), t.CursorPosition); 166 | Assert.AreEqual(1, t.UndoCount); 167 | Assert.AreEqual(1, t.UndoIndex); 168 | 169 | t.CursorPosition = (0, 2); 170 | UndoHelper.TestNopUndo(t, TextEditorModify.Delete); 171 | Assert.AreEqual("bc", t.AllText); 172 | Assert.AreEqual((0, 2), t.CursorPosition); 173 | Assert.AreEqual(1, t.UndoCount); 174 | Assert.AreEqual(1, t.UndoIndex); 175 | 176 | t.Selection.SelectAll(); 177 | Assert.AreEqual((0, 2), t.CursorPosition); 178 | Assert.AreEqual((0, 0), t.Selection.Start); 179 | Assert.AreEqual((0, 2), t.Selection.End); 180 | 181 | UndoHelper.TestUndo(t, TextEditorModify.Delete); 182 | Assert.AreEqual("", t.AllText); 183 | Assert.AreEqual((0, 0), t.CursorPosition); 184 | Assert.AreEqual((0, 0), t.Selection.Start); 185 | Assert.AreEqual((0, 0), t.Selection.End); 186 | Assert.AreEqual(2, t.UndoCount); 187 | Assert.AreEqual(2, t.UndoIndex); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/InsertionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImGuiColorTextEditNet; 3 | using ImGuiColorTextEditNet.Editor; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace TextEdit.Tests; 7 | 8 | [TestClass] 9 | public class InsertionTests 10 | { 11 | [TestMethod] 12 | public void InsertTest1() => InsertTest1Inner(false, false); 13 | 14 | [TestMethod] 15 | public void InsertTest1Bp() => InsertTest1Inner(true, false); 16 | 17 | [TestMethod] 18 | public void InsertTest1Err() => InsertTest1Inner(false, true); 19 | 20 | static void InsertTest1Inner(bool breakpoints, bool errors) 21 | { 22 | var t = new TextEditor(); 23 | if (breakpoints) 24 | t.Breakpoints.Add(0, 1); 25 | if (errors) 26 | t.ErrorMarkers.Add(0, 1); 27 | Assert.AreEqual("", t.AllText); 28 | 29 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, 'a')); 30 | Assert.AreEqual("a", t.AllText); 31 | Assert.AreEqual((0, 1), t.CursorPosition); 32 | Assert.AreEqual(1, t.UndoCount); 33 | Assert.AreEqual(1, t.UndoIndex); 34 | 35 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, 'b')); 36 | Assert.AreEqual("ab", t.AllText); 37 | Assert.AreEqual((0, 2), t.CursorPosition); 38 | Assert.AreEqual(2, t.UndoCount); 39 | Assert.AreEqual(2, t.UndoIndex); 40 | 41 | t.Selection.Select((0, 0), (0, 2)); 42 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, 'c')); 43 | Assert.AreEqual("c", t.AllText); 44 | Assert.AreEqual((0, 1), t.CursorPosition); 45 | Assert.AreEqual(3, t.UndoCount); 46 | Assert.AreEqual(3, t.UndoIndex); 47 | } 48 | 49 | [TestMethod] 50 | public void InsertNewLine() => InsertNewLineInner(false, false); 51 | 52 | [TestMethod] 53 | public void InsertNewLineBp() => InsertNewLineInner(true, false); 54 | 55 | [TestMethod] 56 | public void InsertNewLineErr() => InsertNewLineInner(false, true); 57 | 58 | static void InsertNewLineInner(bool breakpoints, bool errors) 59 | { 60 | var t = new TextEditor(); 61 | if (breakpoints) 62 | t.Breakpoints.Add(0, 1); 63 | 64 | if (errors) 65 | t.ErrorMarkers.Add(0, 1); 66 | 67 | Assert.AreEqual("", t.AllText); 68 | 69 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, '\n')); 70 | Assert.AreEqual(Environment.NewLine, t.AllText); 71 | Assert.AreEqual((1, 0), t.CursorPosition); 72 | Assert.AreEqual(1, t.UndoCount); 73 | Assert.AreEqual(1, t.UndoIndex); 74 | 75 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, 'a')); 76 | Assert.AreEqual(Environment.NewLine + "a", t.AllText); 77 | Assert.AreEqual((1, 1), t.CursorPosition); 78 | Assert.AreEqual(2, t.UndoCount); 79 | Assert.AreEqual(2, t.UndoIndex); 80 | 81 | t.Selection.SelectAll(); 82 | UndoHelper.TestUndo(t, x => TextEditorModify.EnterCharacter(x, '\n')); 83 | Assert.AreEqual(Environment.NewLine, t.AllText); 84 | Assert.AreEqual((1, 0), t.CursorPosition); 85 | Assert.AreEqual(3, t.UndoCount); 86 | Assert.AreEqual(3, t.UndoIndex); 87 | } 88 | 89 | [TestMethod] 90 | public void IndentBlockTest() => IndentBlockTestInner(false, false); 91 | 92 | [TestMethod] 93 | public void IndentBlockTestBp() => IndentBlockTestInner(true, false); 94 | 95 | [TestMethod] 96 | public void IndentBlockTestErr() => IndentBlockTestInner(false, true); 97 | 98 | static void IndentBlockTestInner(bool breakpoints, bool errors) 99 | { 100 | var tab = "\t"; 101 | var before = 102 | $@"void main() // 0 103 | {{ // 1 104 | int a; // 2 105 | for (a = 0; a < 10; a++) // 3 106 | {tab}printf(""%d\n"", a); // 4 107 | }} // 5 108 | "; 109 | 110 | var after = 111 | $@"void main() // 0 112 | {{ // 1 113 | {tab}int a; // 2 114 | {tab}for (a = 0; a < 10; a++) // 3 115 | {tab}{tab}printf(""%d\n"", a); // 4 116 | }} // 5 117 | "; 118 | var t = new TextEditor { AllText = before }; 119 | if (breakpoints) 120 | t.Breakpoints.Add(3, 1); 121 | if (errors) 122 | t.ErrorMarkers.Add(3, 1); 123 | 124 | t.Selection.Select((2, 0), (4, 1)); 125 | UndoHelper.TestUndo(t, x => TextEditorModify.IndentSelection(x, false)); 126 | Assert.AreEqual(after, t.AllText); 127 | Assert.AreEqual(1, t.UndoCount); 128 | Assert.AreEqual(1, t.UndoIndex); 129 | 130 | t.Selection.Select((2, 0), (4, 1)); 131 | UndoHelper.TestUndo(t, x => TextEditorModify.IndentSelection(x, true)); 132 | Assert.AreEqual(before, t.AllText); 133 | Assert.AreEqual(2, t.UndoCount); 134 | Assert.AreEqual(2, t.UndoIndex); 135 | } 136 | 137 | [TestMethod] 138 | public void ReplaceSelectionTest1() 139 | { 140 | // "a" -> "b" (replace all text) 141 | var t = new TextEditor { AllText = "a" }; 142 | t.Selection.Select((0, 0), (0, 1)); 143 | UndoHelper.TestUndo(t, x => TextEditorModify.ReplaceSelection(x, "b")); 144 | Assert.AreEqual("b", t.AllText); 145 | Assert.AreEqual(1, t.UndoCount); 146 | Assert.AreEqual(1, t.UndoIndex); 147 | } 148 | 149 | [TestMethod] 150 | public void ReplaceSelectionTest2() 151 | { 152 | // "a" -> "ab" (insert at end) 153 | var t = new TextEditor { AllText = "a" }; 154 | t.Selection.Cursor = (0, 1); 155 | UndoHelper.TestUndo(t, x => TextEditorModify.ReplaceSelection(x, "b")); 156 | Assert.AreEqual("ab", t.AllText); 157 | Assert.AreEqual(1, t.UndoCount); 158 | Assert.AreEqual(1, t.UndoIndex); 159 | } 160 | 161 | [TestMethod] 162 | public void ReplaceSelectionTest3() 163 | { 164 | // "a" -> "ba" (insert at start) 165 | var t = new TextEditor { AllText = "a" }; 166 | t.Selection.Cursor = (0, 0); 167 | UndoHelper.TestUndo(t, x => TextEditorModify.ReplaceSelection(x, "b")); 168 | Assert.AreEqual("ba", t.AllText); 169 | Assert.AreEqual(1, t.UndoCount); 170 | Assert.AreEqual(1, t.UndoIndex); 171 | } 172 | 173 | [TestMethod] 174 | public void ReplaceSelectionTest4() 175 | { 176 | // "ac" -> "abc" (insert in middle) 177 | var t = new TextEditor { AllText = "ac" }; 178 | t.Selection.Cursor = (0, 1); 179 | UndoHelper.TestUndo(t, x => TextEditorModify.ReplaceSelection(x, "b")); 180 | Assert.AreEqual("abc", t.AllText); 181 | Assert.AreEqual(1, t.UndoCount); 182 | Assert.AreEqual(1, t.UndoIndex); 183 | } 184 | 185 | [TestMethod] 186 | public void ReplaceSelectionTest5() 187 | { 188 | // "abc\ndef" -> "abxyzef" (multi-line replace with partial lines) 189 | // |||| sel 190 | var before = 191 | @"abc 192 | def"; 193 | 194 | var after = "abxyzef"; 195 | 196 | var t = new TextEditor { AllText = before }; 197 | t.Selection.Select((0, 2), (1, 1)); 198 | UndoHelper.TestUndo(t, x => TextEditorModify.ReplaceSelection(x, "xyz")); 199 | Assert.AreEqual(after, t.AllText); 200 | Assert.AreEqual(1, t.UndoCount); 201 | Assert.AreEqual(1, t.UndoIndex); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/TextEdit/TextEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Numerics; 4 | using System.Text.Json; 5 | using ImGuiColorTextEditNet.Editor; 6 | using ImGuiColorTextEditNet.Input; 7 | using ImGuiColorTextEditNet.Syntax; 8 | 9 | // ReSharper disable MemberCanBePrivate.Global 10 | // ReSharper disable UnusedMember.Global 11 | // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global 12 | // ReSharper disable UnusedAutoPropertyAccessor.Global 13 | 14 | namespace ImGuiColorTextEditNet; 15 | 16 | /// Text editor component that provides functionality for editing text with syntax highlighting, undo/redo, selection, and more. 17 | public class TextEditor 18 | { 19 | internal TextEditorUndoStack UndoStack { get; } 20 | internal TextEditorColor Color { get; } 21 | internal TextEditorText Text { get; } 22 | 23 | /// Gets the selection manager, allowing for text selection and manipulation. 24 | public TextEditorSelection Selection { get; } 25 | 26 | /// Gets the options for configuring the text editor's behavior and appearance. 27 | public TextEditorOptions Options { get; } 28 | 29 | /// Gets the breakpoints manager, allowing for setting and managing breakpoints in the text. 30 | public TextEditorBreakpoints Breakpoints { get; } 31 | 32 | /// Gets the error markers manager, allowing for setting and managing error markers in the text. 33 | public TextEditorErrorMarkers ErrorMarkers { get; } 34 | 35 | /// Gets the renderer, responsible for rendering the text and its syntax highlighting. 36 | public TextEditorRenderer Renderer { get; } 37 | 38 | /// Gets the movement manager, allowing for cursor movement and navigation within the text. 39 | public TextEditorMovement Movement { get; } 40 | 41 | /// Initializes a new instance of the class with default options and configurations. 42 | public TextEditor() 43 | { 44 | Options = new(); 45 | Text = new(Options); 46 | Selection = new(Text); 47 | Breakpoints = new(Text); 48 | ErrorMarkers = new(Text); 49 | Color = new(Options, Text); 50 | Movement = new(Selection, Text); 51 | UndoStack = new(Text, Options); 52 | Renderer = new(this, Palettes.Dark) 53 | { 54 | KeyboardInput = new StandardKeyboardInput(this), 55 | MouseInput = new StandardMouseInput(this), 56 | }; 57 | } 58 | 59 | /// Gets the total number of lines in the text editor, excluding the last empty line. 60 | public int TotalLines => Text.LineCount; 61 | 62 | /// Gets or sets the complete text content of the editor, including all lines. 63 | public string AllText 64 | { 65 | get => Text.GetText((0, 0), (Text.LineCount, 0)); 66 | set => Text.SetText(value); 67 | } 68 | 69 | /// Gets or sets the lines of text in the editor. 70 | public IList TextLines 71 | { 72 | get => Text.TextLines; 73 | set => Text.TextLines = value; 74 | } 75 | 76 | /// Appends a line of text to the end of the editor. 77 | public void AppendLine(string text) 78 | { 79 | UndoStack.Clear(); 80 | Text.InsertLine(Text.LineCount - 1, text); 81 | } 82 | 83 | /// Appends a line of text with a specific color to the end of the editor. 84 | public void AppendLine(string text, PaletteIndex color) 85 | { 86 | UndoStack.Clear(); 87 | Text.InsertLine(Text.LineCount - 1, text, color); 88 | } 89 | 90 | /// Appends a span of text to the end of the editor. 91 | public void Append(ReadOnlySpan text, PaletteIndex color) => Text.Append(text, color); 92 | 93 | /// Appends a line of text represented by a object to the end of the editor. 94 | public void AppendLine(Line line) 95 | { 96 | UndoStack.Clear(); 97 | Text.InsertLine(Text.LineCount - 1, line); 98 | } 99 | 100 | /// Gets or sets the syntax highlighter used for syntax highlighting in the text editor. 101 | public ISyntaxHighlighter SyntaxHighlighter 102 | { 103 | get => Color.SyntaxHighlighter; 104 | set => Color.SyntaxHighlighter = value; 105 | } 106 | 107 | /// Sets the color for a specific palette index in the text editor. 108 | public void SetColor(PaletteIndex color, uint abgr) => Renderer.SetColor(color, abgr); 109 | 110 | /// Gets the text of the current line where the cursor is located. 111 | public string GetCurrentLineText() 112 | { 113 | var lineLength = Text.GetLineMaxColumn(Selection.Cursor.Line); 114 | return Text.GetText((Selection.Cursor.Line, 0), (Selection.Cursor.Line, lineLength)); 115 | } 116 | 117 | /// Gets or sets the current cursor position in the text editor. 118 | public Coordinates CursorPosition 119 | { 120 | get => Selection.GetActualCursorCoordinates(); 121 | set 122 | { 123 | if (Selection.Cursor == value) 124 | return; 125 | 126 | Selection.Cursor = value; 127 | Selection.Select(value, value); 128 | ScrollToLine(value.Line); 129 | } 130 | } 131 | 132 | /// Renders the text editor with the specified title and size. Returns true if the text has changed. 133 | public bool Render(string title, Vector2 size = new()) 134 | { 135 | long initialVersion = Text.Version; 136 | Renderer.Render(title, size); 137 | return initialVersion != Text.Version; 138 | } 139 | 140 | /// The version of the text content, for detecting changes. 141 | public long Version => Text.Version; 142 | 143 | /// Undoes the last action in the text editor, allowing for reverting changes made to the text. 144 | public void Undo() => UndoStack.Undo(this); 145 | 146 | /// Redoes the last undone action in the text editor, allowing for reapplying changes that were previously undone. 147 | public void Redo() => UndoStack.Redo(this); 148 | 149 | /// Gets the number of actions that can be undone in the text editor. 150 | public int UndoCount => UndoStack.UndoCount; 151 | 152 | /// Gets the index of the current undo action in the undo stack. 153 | public int UndoIndex => UndoStack.UndoIndex; 154 | 155 | /// Serializes the current state of the text editor, including options, selection, breakpoints, error markers, and text lines, to a JSON string. 156 | public string SerializeState() 157 | { 158 | var state = new 159 | { 160 | Options, 161 | Selection = Selection.SerializeState(), 162 | Breakpoints = Breakpoints.SerializeState(), 163 | ErrorMarkers = ErrorMarkers.SerializeState(), 164 | Text = TextLines, 165 | }; 166 | 167 | return JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }); 168 | } 169 | 170 | /// Scrolls the text editor to a specific line number, making it visible in the viewport. 171 | public void ScrollToLine(int lineNumber) => Text.PendingScrollRequest = lineNumber; 172 | } 173 | -------------------------------------------------------------------------------- /src/TextEdit/Input/StandardKeyboardInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ImGuiColorTextEditNet.Editor; 4 | using ImGuiNET; 5 | 6 | namespace ImGuiColorTextEditNet.Input; 7 | 8 | /// Represents the standard keyboard input handling. 9 | public class StandardKeyboardInput : ITextEditorKeyboardInput 10 | { 11 | readonly TextEditor _editor; 12 | readonly Dictionary _bindings = 13 | []; 14 | 15 | /// Initializes a new instance of the class. 16 | public StandardKeyboardInput(TextEditor editor) 17 | { 18 | _editor = editor ?? throw new ArgumentNullException(nameof(editor)); 19 | AddReadOnlyBinding("Ctrl + Z", e => e.Undo()); 20 | AddReadOnlyBinding("Ctrl + Y", e => e.Redo()); 21 | AddReadOnlyBinding("Delete", TextEditorModify.Delete); 22 | AddReadOnlyBinding("Backspace", TextEditorModify.Backspace); 23 | AddReadOnlyBinding("Ctrl + V", TextEditorModify.Paste); 24 | AddReadOnlyBinding("Ctrl + X", TextEditorModify.Cut); 25 | AddReadOnlyBinding("Enter", e => TextEditorModify.EnterCharacter(e, '\n')); 26 | AddReadOnlyBinding("Tab", e => Indent(false, e)); 27 | AddReadOnlyBinding("Shift + Tab", e => Indent(true, e)); 28 | 29 | AddBinding( 30 | "CapsLock", 31 | (e, _) => 32 | { 33 | if (!ColemakMode) 34 | return false; 35 | 36 | TextEditorModify.Backspace(e); 37 | return true; 38 | } 39 | ); 40 | 41 | AddMutatingBinding("UpArrow", e => e.Movement.MoveUp()); 42 | AddMutatingBinding("Shift + UpArrow", e => e.Movement.MoveUp(1, true)); 43 | 44 | AddMutatingBinding("DownArrow", e => e.Movement.MoveDown()); 45 | AddMutatingBinding("Shift + DownArrow", e => e.Movement.MoveDown(1, true)); 46 | 47 | AddMutatingBinding("LeftArrow", e => e.Movement.MoveLeft()); 48 | AddMutatingBinding("Shift + LeftArrow", e => e.Movement.MoveLeft(1, true)); 49 | AddMutatingBinding("Ctrl + LeftArrow", e => e.Movement.MoveLeft(1, false, true)); 50 | AddMutatingBinding("Ctrl + Shift + LeftArrow", e => e.Movement.MoveLeft(1, true, true)); 51 | 52 | AddMutatingBinding("RightArrow", e => e.Movement.MoveRight()); 53 | AddMutatingBinding("Shift + RightArrow", e => e.Movement.MoveRight(1, true)); 54 | AddMutatingBinding("Ctrl + RightArrow", e => e.Movement.MoveRight(1, false, true)); 55 | AddMutatingBinding("Ctrl + Shift + RightArrow", e => e.Movement.MoveRight(1, true, true)); 56 | 57 | AddMutatingBinding("PageUp", e => e.Movement.MoveUp(e.Renderer.PageSize - 4)); 58 | AddMutatingBinding("Shift + PageUp", e => e.Movement.MoveUp(e.Renderer.PageSize - 4, true)); 59 | AddMutatingBinding("PageDown", e => e.Movement.MoveDown(e.Renderer.PageSize - 4)); 60 | AddMutatingBinding( 61 | "Shift + PageDown", 62 | e => e.Movement.MoveDown(e.Renderer.PageSize - 4, true) 63 | ); 64 | 65 | // BUG: Ctrl+Shift+End doesn't select content on the final line. 66 | 67 | AddMutatingBinding("Home", e => e.Movement.MoveToStartOfLine()); 68 | AddMutatingBinding("End", e => e.Movement.MoveToEndOfLine()); 69 | AddMutatingBinding("Shift+Home", e => e.Movement.MoveToStartOfLine(true)); 70 | AddMutatingBinding("Shift+End", e => e.Movement.MoveToEndOfLine(true)); 71 | AddMutatingBinding("Ctrl + Home", e => e.Movement.MoveToStartOfFile()); 72 | AddMutatingBinding("Ctrl + Shift + Home", e => e.Movement.MoveToStartOfFile(true)); 73 | AddMutatingBinding("Ctrl + End", e => e.Movement.MoveToEndOfFile()); 74 | AddMutatingBinding("Ctrl + Shift + End", e => e.Movement.MoveToEndOfFile(true)); 75 | AddMutatingBinding("Insert", e => e.Options.IsOverwrite = !e.Options.IsOverwrite); 76 | AddMutatingBinding("Ctrl + C", TextEditorModify.Copy); 77 | AddMutatingBinding("Ctrl + A", e => e.Selection.SelectAll()); 78 | 79 | return; 80 | 81 | void Indent(bool shifted, TextEditor e) 82 | { 83 | if (e.Selection.HasSelection && e.Selection.Start.Line != e.Selection.End.Line) 84 | TextEditorModify.IndentSelection(e, shifted); 85 | else 86 | TextEditorModify.EnterCharacter(e, '\t'); 87 | } 88 | } 89 | 90 | /// 91 | /// Removes all key-bindings 92 | /// 93 | public void ClearBindings() => _bindings.Clear(); 94 | 95 | /// 96 | /// Adds a key binding to the text editor with an associated action and context. 97 | /// 98 | public void AddBinding(EditorKeybind binding, object? context, EditorKeybindAction action) => 99 | _bindings[binding] = (context, action); 100 | 101 | /// 102 | /// Adds a key binding to the text editor with an associated action and context. 103 | /// 104 | public void AddBinding(string binding, object? context, EditorKeybindAction action) 105 | { 106 | if (!EditorKeybind.TryParse(binding, out var keybind)) 107 | throw new ArgumentException($"Invalid key binding: {binding}", nameof(binding)); 108 | 109 | AddBinding(keybind, context, action); 110 | } 111 | 112 | /// 113 | /// Adds a key binding to the text editor with an associated action and context. 114 | /// 115 | public void AddBinding(string binding, EditorKeybindAction action) => 116 | AddBinding(binding, null, action); 117 | 118 | /// 119 | /// Adds a simple key binding to the text editor that executes an action without context and is safe to run on a read-only editor. 120 | /// 121 | public void AddReadOnlyBinding(string binding, Action action) => 122 | AddBinding( 123 | binding, 124 | (editor, _) => 125 | { 126 | action(editor); 127 | return true; 128 | } 129 | ); 130 | 131 | /// 132 | /// Adds a read-only key binding to the text editor that executes an action only if the editor is not in read-only mode. 133 | /// 134 | public void AddMutatingBinding(string binding, Action action) => 135 | AddBinding( 136 | binding, 137 | (editor, _) => 138 | { 139 | if (editor.Options.IsReadOnly) 140 | return false; 141 | 142 | action(editor); 143 | return true; 144 | } 145 | ); 146 | 147 | /// Gets or sets a value indicating whether Colemak keyboard layout mode is enabled. 148 | public bool ColemakMode { get; set; } = false; 149 | 150 | /// Handles keyboard inputs for the text editor. 151 | public void HandleKeyboardInputs() 152 | { 153 | if (!ImGui.IsWindowFocused()) 154 | return; 155 | 156 | var io = ImGui.GetIO(); 157 | var shift = io.KeyShift; 158 | var ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; 159 | 160 | io.WantCaptureKeyboard = true; 161 | io.WantTextInput = true; 162 | 163 | foreach (var (binding, value) in _bindings) 164 | if (binding.Ctrl == ctrl && binding.Shift == shift && ImGui.IsKeyPressed(binding.Key)) 165 | if (value.Action(_editor, value.Context)) 166 | break; 167 | 168 | if (!_editor.Options.IsReadOnly && io.InputQueueCharacters.Size != 0) 169 | { 170 | for (int i = 0; i < io.InputQueueCharacters.Size; i++) 171 | { 172 | var c = io.InputQueueCharacters[i]; 173 | if (c != 0 && c is '\n' or >= 32) 174 | TextEditorModify.EnterCharacter(_editor, (char)c); 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/TextEdit/Syntax/RegexSyntaxHighlighter.cs: -------------------------------------------------------------------------------- 1 | /* TODO: Port to C# or replace w/ new code 2 | namespace ImGuiColorTextEditNet; 3 | public class RegexSyntaxHighlighter : ISyntaxHighlighter 4 | { 5 | static readonly object DefaultState = new(); 6 | static readonly object MultiLineCommentState = new(); 7 | public bool AutoIndentation { get; } 8 | public int MaxLinesPerFrame { get; } 9 | public string? GetTooltip(string id) 10 | { 11 | return null; 12 | } 13 | 14 | public object Colorize(List line, object state) 15 | { 16 | return DefaultState; 17 | } 18 | 19 | void ColorizeRange(int aFromLine = 0, int aToLine = 0) 20 | { 21 | if (_lines.Count == 0 || aFromLine >= aToLine) 22 | return; 23 | 24 | string buffer; 25 | std::cmatch results; 26 | string id; 27 | 28 | int endLine = Math.Max(0, Math.Min(_lines.Count, aToLine)); 29 | for (int i = aFromLine; i < endLine; ++i) 30 | { 31 | var line = _lines[i]; 32 | 33 | if (line.Count == 0) 34 | continue; 35 | 36 | buffer.resize(line.Count); 37 | for (int j = 0; j < line.Count; ++j) 38 | { 39 | var col = line[j]; 40 | buffer[j] = col._char; 41 | col._colorIndex = PaletteIndex.Default; 42 | } 43 | 44 | char* bufferBegin = buffer.front(); 45 | char* bufferEnd = bufferBegin + buffer.Count; 46 | 47 | var last = bufferEnd; 48 | 49 | for (var first = bufferBegin; first != last;) 50 | { 51 | char* token_begin = null; 52 | char* token_end = null; 53 | PaletteIndex token_color = PaletteIndex.Default; 54 | 55 | bool hasTokenizeResult = false; 56 | 57 | if (_languageDefinition._tokenize != null) 58 | { 59 | if (_languageDefinition._tokenize(first, last, token_begin, token_end, token_color)) 60 | hasTokenizeResult = true; 61 | } 62 | 63 | if (!hasTokenizeResult) 64 | { 65 | // todo : remove 66 | //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); 67 | 68 | foreach (var p in _regexList) 69 | { 70 | if (std::regex_search(first, last, results, p.first, std::regex_constants::match_continuous)) 71 | { 72 | hasTokenizeResult = true; 73 | 74 | var v = *results.begin(); 75 | token_begin = v.first; 76 | token_end = v.Value; 77 | token_color = p.Value; 78 | break; 79 | } 80 | } 81 | } 82 | 83 | if (!hasTokenizeResult) 84 | { 85 | first++; 86 | } 87 | else 88 | { 89 | int token_length = token_end - token_begin; 90 | 91 | if (token_color == PaletteIndex.Identifier) 92 | { 93 | id.assign(token_begin, token_end); 94 | 95 | // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? 96 | if (!_languageDefinition._caseSensitive) 97 | std::transform(id.begin(), id.end(), id.begin(), ::toupper); 98 | 99 | if (!line[first - bufferBegin]._preprocessor) 100 | { 101 | if (_languageDefinition._keywords.count(id) != 0) 102 | token_color = PaletteIndex.Keyword; 103 | else if (_languageDefinition._identifiers.count(id) != 0) 104 | token_color = PaletteIndex.KnownIdentifier; 105 | else if (_languageDefinition._preprocIdentifiers.count(id) != 0) 106 | token_color = PaletteIndex.PreprocIdentifier; 107 | } 108 | else 109 | { 110 | if (_languageDefinition._preprocIdentifiers.count(id) != 0) 111 | token_color = PaletteIndex.PreprocIdentifier; 112 | } 113 | } 114 | 115 | for (int j = 0; j < token_length; ++j) 116 | line[(token_begin - bufferBegin) + j]._colorIndex = token_color; 117 | 118 | first = token_end; 119 | } 120 | } 121 | } 122 | } 123 | 124 | void ColorizeInternal() 125 | { 126 | if (_lines.Count == 0 || !IsColorizerEnabled) 127 | return; 128 | 129 | if (_checkComments) 130 | { 131 | var endLine = _lines.Count; 132 | var endIndex = 0; 133 | var commentStartLine = endLine; 134 | var commentStartIndex = endIndex; 135 | var withinString = false; 136 | var withinSingleLineComment = false; 137 | var withinPreproc = false; 138 | var firstChar = true; // there is no other non-whitespace characters in the line before 139 | var concatenate = false; // '\' on the very end of the line 140 | var currentLine = 0; 141 | var currentIndex = 0; 142 | while (currentLine < endLine || currentIndex < endIndex) 143 | { 144 | var line = _lines[currentLine]; 145 | 146 | if (currentIndex == 0 && !concatenate) 147 | { 148 | withinSingleLineComment = false; 149 | withinPreproc = false; 150 | firstChar = true; 151 | } 152 | 153 | concatenate = false; 154 | 155 | if (line.Count != 0) 156 | { 157 | var g = line[currentIndex]; 158 | var c = g._char; 159 | 160 | if (c != _languageDefinition._preprocChar && !char.IsWhiteSpace(c)) 161 | firstChar = false; 162 | 163 | if (currentIndex == line.Count - 1 && line[^1]._char == '\\') 164 | concatenate = true; 165 | 166 | bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); 167 | 168 | if (withinString) 169 | { 170 | g._multiLineComment = inComment; 171 | 172 | if (c == '\"') 173 | { 174 | if (currentIndex + 1 < line.Count && line[currentIndex + 1]._char == '\"') 175 | { 176 | currentIndex += 1; 177 | if (currentIndex < line.Count) 178 | g._multiLineComment = inComment; 179 | } 180 | else 181 | withinString = false; 182 | } 183 | else if (c == '\\') 184 | { 185 | currentIndex += 1; 186 | if (currentIndex < line.Count) 187 | g._multiLineComment = inComment; 188 | } 189 | } 190 | else 191 | { 192 | if (firstChar && c == _languageDefinition._preprocChar) 193 | withinPreproc = true; 194 | 195 | if (c == '\"') 196 | { 197 | withinString = true; 198 | g._multiLineComment = inComment; 199 | } 200 | else 201 | { 202 | bool pred(char a, Glyph b) { return a == b._char; } 203 | var from = line.begin() + currentIndex; 204 | var startStr = _languageDefinition._commentStart; 205 | var singleStartStr = _languageDefinition._singleLineComment; 206 | 207 | if (singleStartStr.Count > 0 && 208 | currentIndex + singleStartStr.Count <= line.Count && 209 | equals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.Count, pred)) 210 | { 211 | withinSingleLineComment = true; 212 | } 213 | else if (!withinSingleLineComment && currentIndex + startStr.Count <= line.Count && 214 | equals(startStr.begin(), startStr.end(), from, from + startStr.Count, pred)) 215 | { 216 | commentStartLine = currentLine; 217 | commentStartIndex = currentIndex; 218 | } 219 | 220 | inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); 221 | 222 | g._multiLineComment = inComment; 223 | g._comment = withinSingleLineComment; 224 | 225 | var endStr = _languageDefinition._commentEnd; 226 | if (currentIndex + 1 >= (int)endStr.Count && 227 | equals(endStr.begin(), endStr.end(), from + 1 - endStr.Count, from + 1, pred)) 228 | { 229 | commentStartIndex = endIndex; 230 | commentStartLine = endLine; 231 | } 232 | } 233 | } 234 | 235 | g._preprocessor = withinPreproc; 236 | line[currentIndex] = g; 237 | 238 | currentIndex += UTF8CharLength(c); 239 | if (currentIndex >= line.Count) 240 | { 241 | currentIndex = 0; 242 | ++currentLine; 243 | } 244 | } 245 | else 246 | { 247 | currentIndex = 0; 248 | ++currentLine; 249 | } 250 | } 251 | _checkComments = false; 252 | } 253 | 254 | if (_colorRangeMin < _colorRangeMax) 255 | { 256 | int increment = (_languageDefinition.Tokenize == null) ? 10 : 10000; 257 | int to = Math.Min(_colorRangeMin + increment, _colorRangeMax); 258 | ColorizeRange(_colorRangeMin, to); 259 | _colorRangeMin = to; 260 | 261 | if (_colorRangeMax == _colorRangeMin) 262 | { 263 | _colorRangeMin = int.MaxValue; 264 | _colorRangeMax = 0; 265 | } 266 | return; 267 | } 268 | } 269 | } 270 | */ 271 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorMovement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Editor; 4 | 5 | /// Provides methods for moving the cursor. 6 | public class TextEditorMovement 7 | { 8 | readonly TextEditorSelection _selection; 9 | readonly TextEditorText _text; 10 | 11 | internal TextEditorMovement(TextEditorSelection selection, TextEditorText text) 12 | { 13 | _selection = selection ?? throw new ArgumentNullException(nameof(selection)); 14 | _text = text ?? throw new ArgumentNullException(nameof(text)); 15 | } 16 | 17 | /// Moves the cursor up by a specified amount of lines. 18 | public void MoveUp(int amount = 1, bool isSelecting = false) 19 | { 20 | var oldPos = _selection.Cursor; 21 | var newPos = _selection.Cursor; 22 | newPos.Line = Math.Max(0, _selection.Cursor.Line - amount); 23 | 24 | if (oldPos == newPos) 25 | return; 26 | 27 | _selection.Cursor = newPos; 28 | if (isSelecting) 29 | { 30 | if (oldPos == _selection.InteractiveStart) 31 | _selection.InteractiveStart = _selection.Cursor; 32 | else if (oldPos == _selection.InteractiveEnd) 33 | _selection.InteractiveEnd = _selection.Cursor; 34 | else 35 | { 36 | _selection.InteractiveStart = _selection.Cursor; 37 | _selection.InteractiveEnd = oldPos; 38 | } 39 | } 40 | else 41 | { 42 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 43 | } 44 | 45 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 46 | _text.PendingScrollRequest = _selection.Cursor.Line; 47 | } 48 | 49 | /// Moves the cursor down by a specified amount of lines. 50 | public void MoveDown(int amount = 1, bool isSelecting = false) 51 | { 52 | Util.Assert(_selection.Cursor.Column >= 0); 53 | var oldPos = _selection.Cursor; 54 | var newPos = _selection.Cursor; 55 | newPos.Line = Math.Max(0, Math.Min(_text.LineCount - 1, _selection.Cursor.Line + amount)); 56 | 57 | if (newPos == oldPos) 58 | return; 59 | 60 | _selection.Cursor = newPos; 61 | 62 | if (isSelecting) 63 | { 64 | if (oldPos == _selection.InteractiveEnd) 65 | _selection.InteractiveEnd = _selection.Cursor; 66 | else if (oldPos == _selection.InteractiveStart) 67 | _selection.InteractiveStart = _selection.Cursor; 68 | else 69 | { 70 | _selection.InteractiveStart = oldPos; 71 | _selection.InteractiveEnd = _selection.Cursor; 72 | } 73 | } 74 | else 75 | { 76 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 77 | } 78 | 79 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 80 | _text.PendingScrollRequest = _selection.Cursor.Line; 81 | } 82 | 83 | /// Moves the cursor left by a specified amount of characters or words. 84 | public void MoveLeft(int amount = 1, bool isSelecting = false, bool isWordMode = false) 85 | { 86 | if (_text.LineCount == 0) 87 | return; 88 | 89 | var oldPos = _selection.Cursor; 90 | _selection.Cursor = _selection.GetActualCursorCoordinates(); 91 | var line = _selection.Cursor.Line; 92 | var cindex = _text.GetCharacterIndex(_selection.Cursor); 93 | 94 | while (amount-- > 0) 95 | { 96 | if (cindex == 0) 97 | { 98 | if (line > 0) 99 | { 100 | --line; 101 | cindex = _text.LineCount > line ? _text.GetLine(line).Length : 0; 102 | } 103 | } 104 | else 105 | { 106 | --cindex; 107 | } 108 | 109 | _selection.Cursor = (line, _text.GetCharacterColumn(line, cindex)); 110 | if (isWordMode) 111 | { 112 | _selection.Cursor = _text.FindWordStart(_selection.Cursor); 113 | cindex = _text.GetCharacterIndex(_selection.Cursor); 114 | } 115 | } 116 | 117 | _selection.Cursor = (line, _text.GetCharacterColumn(line, cindex)); 118 | 119 | Util.Assert(_selection.Cursor.Column >= 0); 120 | if (isSelecting) 121 | { 122 | if (oldPos == _selection.InteractiveStart) 123 | _selection.InteractiveStart = _selection.Cursor; 124 | else if (oldPos == _selection.InteractiveEnd) 125 | _selection.InteractiveEnd = _selection.Cursor; 126 | else 127 | { 128 | _selection.InteractiveStart = _selection.Cursor; 129 | _selection.InteractiveEnd = oldPos; 130 | } 131 | } 132 | else 133 | { 134 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 135 | } 136 | 137 | _selection.Select( 138 | _selection.InteractiveStart, 139 | _selection.InteractiveEnd, 140 | isSelecting && isWordMode ? SelectionMode.Word : SelectionMode.Normal 141 | ); 142 | _text.PendingScrollRequest = _selection.Cursor.Line; 143 | } 144 | 145 | /// Moves the cursor right by a specified amount of characters or words. 146 | public void MoveRight(int amount = 1, bool isSelecting = false, bool isWordMode = false) 147 | { 148 | var oldPos = _selection.Cursor; 149 | 150 | if (_text.LineCount == 0 || oldPos.Line >= _text.LineCount) 151 | return; 152 | 153 | var cindex = _text.GetCharacterIndex(_selection.Cursor); 154 | while (amount-- > 0) 155 | { 156 | var lindex = _selection.Cursor.Line; 157 | var line = _text.GetLine(lindex); 158 | if (cindex >= line.Length) 159 | { 160 | if (_selection.Cursor.Line < _text.LineCount - 1) 161 | { 162 | _selection.Cursor = ( 163 | Math.Max(0, Math.Min(_text.LineCount - 1, _selection.Cursor.Line + 1)), 164 | 0 165 | ); 166 | } 167 | else 168 | { 169 | return; 170 | } 171 | } 172 | else 173 | { 174 | cindex++; 175 | 176 | _selection.Cursor = isWordMode 177 | ? _text.FindNextWord(_selection.Cursor) 178 | : (lindex, _text.GetCharacterColumn(lindex, cindex)); 179 | } 180 | } 181 | 182 | if (isSelecting) 183 | { 184 | if (oldPos == _selection.InteractiveEnd) 185 | _selection.InteractiveEnd = _text.SanitizeCoordinates(_selection.Cursor); 186 | else if (oldPos == _selection.InteractiveStart) 187 | _selection.InteractiveStart = _selection.Cursor; 188 | else 189 | { 190 | _selection.InteractiveStart = oldPos; 191 | _selection.InteractiveEnd = _selection.Cursor; 192 | } 193 | } 194 | else 195 | { 196 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 197 | } 198 | 199 | _selection.Select( 200 | _selection.InteractiveStart, 201 | _selection.InteractiveEnd, 202 | isSelecting && isWordMode ? SelectionMode.Word : SelectionMode.Normal 203 | ); 204 | 205 | _text.PendingScrollRequest = _selection.Cursor.Line; 206 | } 207 | 208 | /// Moves the cursor to the start of the file, optionally selecting text from the previous position to the new position. 209 | public void MoveToStartOfFile(bool isSelecting = false) 210 | { 211 | var oldPos = _selection.Cursor; 212 | _selection.Cursor = (0, 0); 213 | 214 | if (_selection.Cursor == oldPos) 215 | return; 216 | 217 | if (isSelecting) 218 | { 219 | _selection.InteractiveEnd = oldPos; 220 | _selection.InteractiveStart = _selection.Cursor; 221 | } 222 | else 223 | { 224 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 225 | } 226 | 227 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 228 | _text.PendingScrollRequest = _selection.Cursor.Line; 229 | } 230 | 231 | /// Moves the cursor to the end of the file, optionally selecting text from the previous position to the new position. 232 | public void MoveToEndOfFile(bool isSelecting = false) 233 | { 234 | var oldPos = _selection.Cursor; 235 | var newPos = (_text.LineCount - 1, 0); 236 | _selection.Cursor = newPos; 237 | 238 | if (isSelecting) 239 | { 240 | _selection.InteractiveStart = oldPos; 241 | _selection.InteractiveEnd = newPos; 242 | } 243 | else 244 | { 245 | _selection.InteractiveStart = _selection.InteractiveEnd = newPos; 246 | } 247 | 248 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 249 | _text.PendingScrollRequest = _selection.Cursor.Line; 250 | } 251 | 252 | /// Moves the cursor to the start of the current line, optionally selecting text from the previous position to the new position. 253 | public void MoveToStartOfLine(bool isSelecting = false) 254 | { 255 | var oldPos = _selection.Cursor; 256 | _selection.Cursor = (_selection.Cursor.Line, 0); 257 | 258 | if (_selection.Cursor != oldPos) 259 | { 260 | if (isSelecting) 261 | { 262 | if (oldPos == _selection.InteractiveStart) 263 | _selection.InteractiveStart = _selection.Cursor; 264 | else if (oldPos == _selection.InteractiveEnd) 265 | _selection.InteractiveEnd = _selection.Cursor; 266 | else 267 | { 268 | _selection.InteractiveStart = _selection.Cursor; 269 | _selection.InteractiveEnd = oldPos; 270 | } 271 | } 272 | else 273 | { 274 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 275 | } 276 | 277 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 278 | } 279 | 280 | _text.PendingScrollRequest = _selection.Cursor.Line; 281 | } 282 | 283 | /// Moves the cursor to the end of the current line, optionally selecting text from the previous position to the new position. 284 | public void MoveToEndOfLine(bool isSelecting = false) 285 | { 286 | var oldPos = _selection.Cursor; 287 | _selection.Cursor = (_selection.Cursor.Line, _text.GetLineMaxColumn(oldPos.Line)); 288 | 289 | if (_selection.Cursor == oldPos) 290 | return; 291 | 292 | if (isSelecting) 293 | { 294 | if (oldPos == _selection.InteractiveEnd) 295 | _selection.InteractiveEnd = _selection.Cursor; 296 | else if (oldPos == _selection.InteractiveStart) 297 | _selection.InteractiveStart = _selection.Cursor; 298 | else 299 | { 300 | _selection.InteractiveStart = oldPos; 301 | _selection.InteractiveEnd = _selection.Cursor; 302 | } 303 | } 304 | else 305 | { 306 | _selection.InteractiveStart = _selection.InteractiveEnd = _selection.Cursor; 307 | } 308 | 309 | _selection.Select(_selection.InteractiveStart, _selection.InteractiveEnd); 310 | _text.PendingScrollRequest = _selection.Cursor.Line; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorModify.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using ImGuiColorTextEditNet.Operations; 4 | using ImGuiNET; 5 | 6 | namespace ImGuiColorTextEditNet.Editor; 7 | 8 | /// Provides methods for modifying text, e.g. copy, cut, paste, delete, and character entry. 9 | public static class TextEditorModify 10 | { 11 | static readonly SimpleCache CharLabelCache = new( 12 | "char strings", 13 | x => x.ToString() 14 | ); 15 | 16 | /// Copies the currently selected text to the clipboard. 17 | public static void Copy(TextEditor e) 18 | { 19 | if (e.Selection.HasSelection) 20 | { 21 | ImGui.SetClipboardText(e.Selection.GetSelectedText()); 22 | return; 23 | } 24 | 25 | if (e.Text.LineCount == 0) 26 | return; 27 | 28 | StringBuilder sb = new(); 29 | 30 | var line = e.Text.GetLine(e.Selection.GetActualCursorCoordinates().Line); 31 | foreach (var g in line) 32 | sb.Append(g.Char); 33 | 34 | ImGui.SetClipboardText(sb.ToString()); 35 | } 36 | 37 | /// Cuts the currently selected text, copying it to the clipboard and removing it from the editor. 38 | public static void Cut(TextEditor e) 39 | { 40 | if (e.Options.IsReadOnly) 41 | { 42 | Copy(e); 43 | return; 44 | } 45 | 46 | if (!e.Selection.HasSelection) 47 | return; 48 | 49 | Copy(e); 50 | UndoRecord undo = DeleteSelection(e); 51 | e.UndoStack.AddUndo(undo); 52 | } 53 | 54 | /// Pastes text from the clipboard into the editor at the current cursor position or replaces the selection if any exists. 55 | public static void Paste(TextEditor e) 56 | { 57 | unsafe 58 | { 59 | if (ImGuiNative.igGetClipboardText() == null) 60 | return; 61 | } 62 | 63 | var clipText = ImGui.GetClipboardText(); 64 | if (string.IsNullOrEmpty(clipText)) 65 | return; 66 | 67 | ReplaceSelection(e, clipText); 68 | } 69 | 70 | /// Replaces the currently selected text with the specified text, or inserts the text at the cursor position if no selection exists. 71 | public static void ReplaceSelection(TextEditor e, string text) 72 | { 73 | Util.Assert(!e.Options.IsReadOnly); 74 | 75 | UndoRecord u = e.Selection.HasSelection 76 | ? DeleteSelection(e) 77 | : new() { Before = e.Selection.State }; 78 | 79 | var pos = e.Selection.GetActualCursorCoordinates(); 80 | u.Added = text; 81 | u.AddedStart = pos; 82 | u.AddedEnd = pos; 83 | 84 | if (!string.IsNullOrEmpty(text)) 85 | { 86 | var start = pos < e.Selection.Start ? pos : e.Selection.Start; 87 | 88 | u.AddedEnd = e.Text.InsertTextAt(pos, text); 89 | 90 | e.Selection.Select(pos, pos); 91 | e.Selection.Cursor = pos; 92 | e.Color.InvalidateColor(start.Line - 1, u.AddedEnd.Line - start.Line + 1); 93 | } 94 | 95 | u.After = e.Selection.State; 96 | e.UndoStack.AddUndo(u); 97 | } 98 | 99 | /// Deletes the currently selected text or the character at the cursor position if no selection exists. 100 | public static void Delete(TextEditor e) 101 | { 102 | if (e.Options.IsReadOnly) 103 | return; 104 | 105 | if (e.Text.LineCount == 0) 106 | return; 107 | 108 | if (e.Selection.HasSelection) 109 | { 110 | UndoRecord u = DeleteSelection(e); 111 | e.UndoStack.AddUndo(u); 112 | } 113 | else 114 | { 115 | var pos = e.Selection.GetActualCursorCoordinates(); 116 | e.Selection.Cursor = pos; 117 | 118 | UndoRecord u = new() { Before = e.Selection.State }; 119 | 120 | if (pos.Column == e.Text.GetLineMaxColumn(pos.Line)) 121 | { 122 | if (pos.Line == e.Text.LineCount - 1) 123 | return; 124 | 125 | u.Removed = "\n"; 126 | u.RemovedStart = u.RemovedEnd = e.Selection.GetActualCursorCoordinates(); 127 | e.Text.Advance(u.RemovedEnd); 128 | e.Text.AppendToLine(pos.Line, e.Text.GetLineText(pos.Line + 1)); 129 | e.Text.RemoveLine(pos.Line + 1); 130 | } 131 | else 132 | { 133 | var cindex = e.Text.GetCharacterIndex(pos); 134 | u.RemovedStart = u.RemovedEnd = e.Selection.GetActualCursorCoordinates(); 135 | u.RemovedEnd.Column++; 136 | u.Removed = e.Text.GetText(u.RemovedStart, u.RemovedEnd); 137 | 138 | e.Text.RemoveInLine(pos.Line, cindex, cindex + 1); 139 | } 140 | 141 | u.After = e.Selection.State; 142 | e.Color.InvalidateColor(pos.Line, 1); 143 | e.UndoStack.AddUndo(u); 144 | } 145 | } 146 | 147 | /// Inserts a character at the current cursor position or replaces the selection if any exists. 148 | public static void EnterCharacter(TextEditor e, char c) 149 | { 150 | Util.Assert(!e.Options.IsReadOnly); 151 | UndoRecord u = e.Selection.HasSelection 152 | ? DeleteSelection(e) 153 | : new() { Before = e.Selection.State }; 154 | 155 | var coord = e.Selection.GetActualCursorCoordinates(); 156 | u.AddedStart = coord; 157 | 158 | Util.Assert(e.Text.LineCount != 0); 159 | 160 | if (c == '\n') 161 | { 162 | var line = e.Text.GetLine(coord.Line); 163 | var newLine = new Line(); 164 | 165 | if (e.Color.SyntaxHighlighter.AutoIndentation) 166 | { 167 | for ( 168 | int it = 0; 169 | it < line.Length 170 | && char.IsAscii(line[it].Char) 171 | && TextEditorText.IsBlank(line[it].Char); 172 | ++it 173 | ) 174 | { 175 | newLine.Glyphs.Add(line[it]); 176 | } 177 | } 178 | 179 | int whitespaceSize = newLine.Glyphs.Count; 180 | var cindex = e.Text.GetCharacterIndex(coord); 181 | foreach (var glyph in line[cindex..]) 182 | newLine.Glyphs.Add(glyph); 183 | 184 | e.Text.InsertLine(coord.Line + 1, newLine); 185 | e.Text.RemoveInLine(coord.Line, cindex, line.Length); 186 | e.Selection.Cursor = ( 187 | coord.Line + 1, 188 | e.Text.GetCharacterColumn(coord.Line + 1, whitespaceSize) 189 | ); 190 | 191 | u.Added = "\n"; 192 | } 193 | else 194 | { 195 | var line = e.Text.GetLine(coord.Line); 196 | var cindex = e.Text.GetCharacterIndex(coord); 197 | 198 | if (e.Options.IsOverwrite && cindex < line.Length) 199 | { 200 | u.RemovedStart = e.Selection.Cursor; 201 | u.RemovedEnd = (coord.Line, e.Text.GetCharacterColumn(coord.Line, cindex + 1)); 202 | 203 | u.Removed += line[cindex].Char; 204 | e.Text.RemoveInLine(coord.Line, cindex, cindex + 1); 205 | } 206 | 207 | e.Text.InsertCharAt(coord, c); 208 | u.Added = CharLabelCache.Get(c); 209 | 210 | e.Selection.Cursor = (coord.Line, e.Text.GetCharacterColumn(coord.Line, cindex + 1)); 211 | } 212 | 213 | u.AddedEnd = e.Selection.GetActualCursorCoordinates(); 214 | u.After = e.Selection.State; 215 | 216 | e.UndoStack.AddUndo(u); 217 | 218 | e.Color.InvalidateColor(coord.Line - 1, 3); 219 | e.Text.PendingScrollRequest = coord.Line; 220 | } 221 | 222 | static (Coordinates, Coordinates) GetIndentRange(TextEditor e) 223 | { 224 | var start = e.Selection.Start; 225 | var end = e.Selection.End; 226 | 227 | if (start > end) 228 | (start, end) = (end, start); 229 | 230 | start.Column = 0; 231 | // end._column = end._line < e.Text.LineCount ? _state._lines[end._line].Count : 0; 232 | if (end is { Column: 0, Line: > 0 }) 233 | --end.Line; 234 | 235 | if (end.Line >= e.Text.LineCount) 236 | end.Line = e.Text.LineCount == 0 ? 0 : e.Text.LineCount - 1; 237 | 238 | end.Column = e.Text.GetLineMaxColumn(end.Line); 239 | 240 | //if (end._column >= GetLineMaxColumn(end._line)) 241 | // end._column = GetLineMaxColumn(end._line) - 1; 242 | 243 | return (start, end); 244 | } 245 | 246 | /// Indents or unindents the selected lines based on the current tab size. 247 | public static void IndentSelection(TextEditor e, bool shift) 248 | { 249 | Util.Assert(!e.Options.IsReadOnly); 250 | 251 | var u = new MetaOperation { Before = e.Selection.State }; 252 | var originalEnd = e.Selection.End; 253 | var (start, end) = GetIndentRange(e); 254 | 255 | for (int i = start.Line; i <= end.Line; i++) 256 | { 257 | if (shift) 258 | { 259 | UnindentLine(e, i, u); 260 | } 261 | else 262 | { 263 | var indentString = e.Options.IndentWithSpaces 264 | ? new string(' ', e.Options.TabSize) 265 | : "\t"; 266 | 267 | u.Add( 268 | new ModifyLineOperation 269 | { 270 | Line = i, 271 | AddedColumn = 0, 272 | Added = indentString, 273 | } 274 | ); 275 | } 276 | } 277 | 278 | if (u.Count == 0) 279 | return; 280 | 281 | e.Selection.Start = (start.Line, e.Text.GetCharacterColumn(start.Line, 0)); 282 | e.Selection.End = 283 | originalEnd.Column != 0 284 | ? (end.Line, e.Text.GetLineMaxColumn(end.Line)) 285 | : (originalEnd.Line, 0); 286 | 287 | e.Text.PendingScrollRequest = e.Selection.End.Line; 288 | 289 | u.After = e.Selection.State; 290 | e.UndoStack.Do(u, e); 291 | } 292 | 293 | static void UnindentLine(TextEditor e, int i, MetaOperation u) 294 | { 295 | ReadOnlySpan line = e.Text.GetLine(i); 296 | if (line.Length == 0) 297 | return; 298 | 299 | string removed; 300 | if (line[0].Char == '\t') 301 | { 302 | removed = e.Text.GetText((i, 0), (i, 1)); 303 | } 304 | else 305 | { 306 | int j = 0; 307 | for (; j < e.Options.TabSize && line.Length != 0 && line[0].Char == ' '; j++) { } 308 | 309 | if (j == 0) 310 | return; 311 | 312 | removed = new string(' ', j); 313 | } 314 | 315 | u.Add( 316 | new ModifyLineOperation 317 | { 318 | Line = i, 319 | RemovedColumn = 0, 320 | Removed = removed, 321 | } 322 | ); 323 | } 324 | 325 | /// Deletes the character before the cursor position or the selected text if any exists. 326 | public static void Backspace(TextEditor e) 327 | { 328 | Util.Assert(!e.Options.IsReadOnly); 329 | 330 | if (e.Text.LineCount == 0) 331 | return; 332 | 333 | if (e.Selection.HasSelection) 334 | { 335 | UndoRecord undo = DeleteSelection(e); 336 | e.UndoStack.AddUndo(undo); 337 | return; 338 | } 339 | 340 | MetaOperation u = new() { Before = e.Selection.State }; 341 | var pos = e.Selection.GetActualCursorCoordinates(); 342 | e.Selection.Cursor = pos; 343 | 344 | if (e.Selection.Cursor.Column == 0) 345 | { 346 | if (e.Selection.Cursor.Line == 0) 347 | return; 348 | 349 | int lineNum = e.Selection.Cursor.Line; 350 | var lineText = e.Text.GetLineText(lineNum); 351 | var prevSize = e.Text.GetLineMaxColumn(lineNum - 1); 352 | 353 | u.Add( 354 | new ModifyLineOperation 355 | { 356 | Line = lineNum - 1, 357 | Added = lineText, 358 | AddedColumn = prevSize, 359 | } 360 | ); 361 | 362 | u.Add(new RemoveLineOperation { Line = lineNum, Removed = lineText }); 363 | u.After.Cursor = (e.Selection.Cursor.Line - 1, prevSize); 364 | } 365 | else 366 | { 367 | var cindex = e.Text.GetCharacterIndex(pos) - 1; 368 | var removed = e.Text.GetText(pos - (0, 1), pos); 369 | 370 | u.Add( 371 | new ModifyLineOperation 372 | { 373 | Line = e.Selection.Cursor.Line, 374 | RemovedColumn = cindex, 375 | Removed = removed, 376 | } 377 | ); 378 | 379 | u.After.Cursor = (e.Selection.Cursor.Line, e.Selection.Cursor.Column - 1); 380 | } 381 | 382 | u.After.Start = u.After.End = u.After.Cursor; 383 | u.Apply(e); 384 | 385 | e.Text.PendingScrollRequest = e.Selection.Cursor.Line; 386 | e.UndoStack.AddUndo(u); 387 | } 388 | 389 | static UndoRecord DeleteSelection(TextEditor e) 390 | { 391 | Util.Assert(e.Selection.End >= e.Selection.Start); 392 | 393 | UndoRecord undo = new() 394 | { 395 | Before = e.Selection.State, 396 | Removed = e.Selection.GetSelectedText(), 397 | RemovedStart = e.Selection.Start, 398 | RemovedEnd = e.Selection.End, 399 | }; 400 | 401 | if (e.Selection.End != e.Selection.Start) 402 | { 403 | e.Text.DeleteRange(e.Selection.Start, e.Selection.End); 404 | 405 | e.Selection.Select(e.Selection.Start, e.Selection.Start); 406 | e.Selection.Cursor = e.Selection.Start; 407 | e.Color.InvalidateColor(e.Selection.Start.Line, 1); 408 | } 409 | 410 | undo.After = e.Selection.State; 411 | return undo; 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/TextEdit/Syntax/CStyleHighlighter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImGuiColorTextEditNet.Syntax; 4 | 5 | /// 6 | /// A syntax highlighter for C and C++ style languages. 7 | /// 8 | public class CStyleHighlighter : ISyntaxHighlighter 9 | { 10 | static readonly object DefaultState = new(); 11 | static readonly object MultiLineCommentState = new(); 12 | readonly SimpleTrie _identifiers; 13 | 14 | record Identifier(PaletteIndex Color) 15 | { 16 | public string Declaration = ""; 17 | } 18 | 19 | /// 20 | /// Creates a new instance of the CStyleHighlighter. 21 | /// 22 | /// true for C++, false for C 23 | public CStyleHighlighter(bool useCpp) 24 | { 25 | var language = useCpp ? CPlusPlus() : C(); 26 | 27 | _identifiers = new(); 28 | if (language.Keywords != null) 29 | foreach (var keyword in language.Keywords) 30 | _identifiers.Add(keyword, new(PaletteIndex.Keyword)); 31 | 32 | if (language.Identifiers != null) 33 | { 34 | foreach (var name in language.Identifiers) 35 | { 36 | var identifier = new Identifier(PaletteIndex.KnownIdentifier) 37 | { 38 | Declaration = "Built-in function", 39 | }; 40 | _identifiers.Add(name, identifier); 41 | } 42 | } 43 | } 44 | 45 | /// Indicates whether the highlighter supports auto-indentation. 46 | public bool AutoIndentation => true; 47 | 48 | /// The maximum number of lines that can be processed in a single frame. 49 | public int MaxLinesPerFrame => 1000; 50 | 51 | /// Retrieves the tooltip for a given identifier. 52 | public string? GetTooltip(string id) 53 | { 54 | var info = _identifiers.Get(id); 55 | return info?.Declaration; 56 | } 57 | 58 | /// Colorizes a line of text based on C/C++ syntax rules. 59 | public object Colorize(Span line, object? state) 60 | { 61 | for (int i = 0; i < line.Length; ) 62 | { 63 | int result = Tokenize(line[i..], ref state); 64 | Util.Assert(result != 0); 65 | 66 | if (result == -1) 67 | { 68 | line[i] = new(line[i].Char, PaletteIndex.Default); 69 | i++; 70 | } 71 | else 72 | i += result; 73 | } 74 | 75 | return state ?? DefaultState; 76 | } 77 | 78 | int Tokenize(Span span, ref object? state) 79 | { 80 | int i = 0; 81 | 82 | // Skip leading whitespace 83 | while (i < span.Length && span[i].Char is ' ' or '\t') 84 | i++; 85 | 86 | if (i > 0) 87 | return i; 88 | 89 | int result; 90 | if ((result = TokenizeMultiLineComment(span, ref state)) != -1) 91 | return result; 92 | 93 | if ((result = TokenizeSingleLineComment(span)) != -1) 94 | return result; 95 | 96 | if ((result = TokenizePreprocessorDirective(span)) != -1) 97 | return result; 98 | 99 | if ((result = TokenizeCStyleString(span)) != -1) 100 | return result; 101 | 102 | if ((result = TokenizeCStyleCharacterLiteral(span)) != -1) 103 | return result; 104 | 105 | if ((result = TokenizeCStyleIdentifier(span)) != -1) 106 | return result; 107 | 108 | if ((result = TokenizeCStyleNumber(span)) != -1) 109 | return result; 110 | 111 | if ((result = TokenizeCStylePunctuation(span)) != -1) 112 | return result; 113 | 114 | return -1; 115 | } 116 | 117 | static int TokenizeMultiLineComment(Span span, ref object? state) 118 | { 119 | int i = 0; 120 | if ( 121 | state != MultiLineCommentState 122 | && (span[i].Char != '/' || 1 >= span.Length || span[1].Char != '*') 123 | ) 124 | { 125 | return -1; 126 | } 127 | 128 | state = MultiLineCommentState; 129 | for (; i < span.Length; i++) 130 | { 131 | span[i] = new(span[i].Char, PaletteIndex.MultiLineComment); 132 | if (span[i].Char == '*' && i + 1 < span.Length && span[i + 1].Char == '/') 133 | { 134 | i++; 135 | span[i] = new(span[i].Char, PaletteIndex.MultiLineComment); 136 | state = DefaultState; 137 | return i; 138 | } 139 | } 140 | 141 | return i; 142 | } 143 | 144 | static int TokenizeSingleLineComment(Span span) 145 | { 146 | if (span[0].Char != '/' || 1 >= span.Length || span[1].Char != '/') 147 | return -1; 148 | 149 | for (int i = 0; i < span.Length; i++) 150 | span[i] = new(span[i].Char, PaletteIndex.Comment); 151 | 152 | return span.Length; 153 | } 154 | 155 | static int TokenizePreprocessorDirective(Span span) 156 | { 157 | if (span[0].Char != '#') 158 | return -1; 159 | 160 | for (int i = 0; i < span.Length; i++) 161 | span[i] = new(span[i].Char, PaletteIndex.Preprocessor); 162 | 163 | return span.Length; 164 | } 165 | 166 | // csharpier-ignore-start 167 | static LanguageDefinition C() => 168 | new("C") 169 | { 170 | Keywords = 171 | [ 172 | "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", 173 | "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", 174 | "restrict", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", 175 | "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", 176 | "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local", 177 | ], 178 | Identifiers = 179 | [ 180 | "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", 181 | "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", 182 | "isalnum", "isalpha", "isdigit", "isgraph", "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", 183 | "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", 184 | "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", 185 | ], 186 | }; 187 | 188 | static LanguageDefinition CPlusPlus() => 189 | new("C++") 190 | { 191 | Keywords = 192 | [ 193 | "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", 194 | "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", "compl", 195 | "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", 196 | "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", "for", "friend", 197 | "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", 198 | "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", 199 | "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", 200 | "struct", "switch", "synchronized", "template", "this", "thread_local", "throw", "true", "try", "typedef", 201 | "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", 202 | "xor", "xor_eq", 203 | ], 204 | Identifiers = 205 | [ 206 | "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", 207 | "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", 208 | "isalnum", "isalpha", "isdigit", "isgraph", "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", 209 | "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", 210 | "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", 211 | "tolower", "toupper", "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", 212 | "max", 213 | ], 214 | }; 215 | // csharpier-ignore-end 216 | 217 | static int TokenizeCStyleString(Span input) 218 | { 219 | if (input[0].Char != '"') 220 | return -1; // No opening quotes 221 | 222 | for (int i = 1; i < input.Length; i++) 223 | { 224 | var c = input[i].Char; 225 | 226 | // handle end of string 227 | if (c == '"') 228 | { 229 | for (int j = 0; j < i; j++) 230 | input[i] = new(c, PaletteIndex.String); 231 | 232 | return i; 233 | } 234 | 235 | // handle escape character for " 236 | if (c == '\\' && i + 1 < input.Length && input[i + 1].Char == '"') 237 | i++; 238 | } 239 | 240 | return -1; // No closing quotes 241 | } 242 | 243 | static int TokenizeCStyleCharacterLiteral(Span input) 244 | { 245 | int i = 0; 246 | 247 | if (input[i++].Char != '\'') 248 | return -1; 249 | 250 | if (i < input.Length && input[i].Char == '\\') 251 | i++; // handle escape characters 252 | 253 | i++; // Skip actual char 254 | 255 | // handle end of character literal 256 | if (i >= input.Length || input[i].Char != '\'') 257 | return -1; 258 | 259 | for (int j = 0; j < i; j++) 260 | input[j] = new(input[j].Char, PaletteIndex.CharLiteral); 261 | 262 | return i; 263 | } 264 | 265 | int TokenizeCStyleIdentifier(Span input) 266 | { 267 | int i = 0; 268 | 269 | var c = input[i].Char; 270 | if (!char.IsLetter(c) && c != '_') 271 | return -1; 272 | 273 | i++; 274 | 275 | for (; i < input.Length; i++) 276 | { 277 | c = input[i].Char; 278 | if (c != '_' && !char.IsLetterOrDigit(c)) 279 | break; 280 | } 281 | 282 | var info = _identifiers.Get(input[..i], x => x.Char); 283 | 284 | for (int j = 0; j < i; j++) 285 | input[j] = new(input[j].Char, info?.Color ?? PaletteIndex.Identifier); 286 | 287 | return i; 288 | } 289 | 290 | static int TokenizeCStyleNumber(Span input) 291 | { 292 | int i = 0; 293 | char c = input[i].Char; 294 | 295 | bool startsWithNumber = char.IsNumber(c); 296 | 297 | if (c != '+' && c != '-' && !startsWithNumber) 298 | return -1; 299 | 300 | i++; 301 | 302 | bool hasNumber = startsWithNumber; 303 | while (i < input.Length && char.IsNumber(input[i].Char)) 304 | { 305 | hasNumber = true; 306 | i++; 307 | } 308 | 309 | if (!hasNumber) 310 | return -1; 311 | 312 | bool isFloat = false; 313 | bool isHex = false; 314 | bool isBinary = false; 315 | 316 | if (i < input.Length) 317 | { 318 | if (input[i].Char == '.') 319 | { 320 | isFloat = true; 321 | 322 | i++; 323 | while (i < input.Length && char.IsNumber(input[i].Char)) 324 | i++; 325 | } 326 | else if (input[i].Char is 'x' or 'X' && i == 1 && input[i].Char == '0') 327 | { 328 | // hex formatted integer of the type 0xef80 329 | isHex = true; 330 | 331 | i++; 332 | for (; i < input.Length; i++) 333 | { 334 | c = input[i].Char; 335 | if ( 336 | !char.IsNumber(c) 337 | && c is not (>= 'a' and <= 'f') 338 | && c is not (>= 'A' and <= 'F') 339 | ) 340 | { 341 | break; 342 | } 343 | } 344 | } 345 | else if (input[i].Char is 'b' or 'B' && i == 1 && input[i].Char == '0') 346 | { 347 | // binary formatted integer of the type 0b01011101 348 | 349 | isBinary = true; 350 | 351 | i++; 352 | for (; i < input.Length; i++) 353 | { 354 | c = input[i].Char; 355 | if (c != '0' && c != '1') 356 | break; 357 | } 358 | } 359 | } 360 | 361 | if (!isHex && !isBinary) 362 | { 363 | // floating point exponent 364 | if (i < input.Length && input[i].Char is 'e' or 'E') 365 | { 366 | isFloat = true; 367 | 368 | i++; 369 | 370 | if (i < input.Length && input[i].Char is '+' or '-') 371 | i++; 372 | 373 | bool hasDigits = false; 374 | while (i < input.Length && input[i].Char is >= '0' and <= '9') 375 | { 376 | hasDigits = true; 377 | i++; 378 | } 379 | 380 | if (!hasDigits) 381 | return -1; 382 | } 383 | 384 | // single precision floating point type 385 | if (i < input.Length && input[i].Char == 'f') 386 | i++; 387 | } 388 | 389 | if (!isFloat) 390 | { 391 | // integer size type 392 | while (i < input.Length && input[i].Char is 'u' or 'U' or 'l' or 'L') 393 | i++; 394 | } 395 | 396 | return i; 397 | } 398 | 399 | static int TokenizeCStylePunctuation(Span input) 400 | { 401 | // csharpier-ignore-start 402 | switch (input[0].Char) 403 | { 404 | case '[': case ']': case '{': case '}': case '(': case ')': case '-': case '+': case '<': case '>': case '?': case ':': 405 | case ';': case '!': case '%': case '^': case '&': case '|': case '*': case '/': case '=': case '~': case ',': case '.': 406 | input[0] = new(input[0].Char, PaletteIndex.Punctuation); 407 | return 1; 408 | 409 | default: 410 | return -1; 411 | } 412 | // csharpier-ignore-end 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/TextEdit.Tests/MovementTests.cs: -------------------------------------------------------------------------------- 1 | using ImGuiColorTextEditNet; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace TextEdit.Tests; 5 | 6 | [TestClass] 7 | public class MovementTests 8 | { 9 | [TestMethod] 10 | public void MoveLeftTest1() 11 | { 12 | var t = new TextEditor 13 | { 14 | AllText = """ 15 | 16 | one two three 17 | 3.14 18 | test.com 19 | 20 | """, 21 | }; 22 | 23 | Assert.AreEqual((0, 0), t.CursorPosition); 24 | Assert.AreEqual((0, 0), t.Selection.Start); 25 | Assert.AreEqual((0, 0), t.Selection.End); 26 | 27 | // Moving left at the start of the document should be a no-op 28 | t.Movement.MoveLeft(); 29 | Assert.AreEqual((0, 0), t.CursorPosition); 30 | Assert.AreEqual((0, 0), t.Selection.Start); 31 | Assert.AreEqual((0, 0), t.Selection.End); 32 | } 33 | 34 | [TestMethod] 35 | public void MoveLeftTest2() 36 | { 37 | var t = new TextEditor 38 | { 39 | AllText = """ 40 | 41 | one two three 42 | 3.14 43 | test.com 44 | 45 | """, 46 | }; 47 | 48 | // Moving left at the start of the document two places should also be a no-op (and not error) 49 | t.Movement.MoveLeft(2); 50 | Assert.AreEqual((0, 0), t.CursorPosition); 51 | Assert.AreEqual((0, 0), t.Selection.Start); 52 | Assert.AreEqual((0, 0), t.Selection.End); 53 | } 54 | 55 | [TestMethod] 56 | public void MoveLeftTest3() 57 | { 58 | var t = new TextEditor 59 | { 60 | AllText = """ 61 | 62 | one two three 63 | 3.14 64 | test.com 65 | 66 | """, 67 | CursorPosition = (1, 0), 68 | }; 69 | 70 | Assert.AreEqual((1, 0), t.CursorPosition); 71 | Assert.AreEqual((1, 0), t.Selection.Start); 72 | Assert.AreEqual((1, 0), t.Selection.End); 73 | 74 | // Moving left at the start of a line should move to the end of the previous line 75 | t.Movement.MoveLeft(); 76 | Assert.AreEqual((0, 0), t.CursorPosition); 77 | Assert.AreEqual((0, 0), t.Selection.Start); 78 | Assert.AreEqual((0, 0), t.Selection.End); 79 | } 80 | 81 | [TestMethod] 82 | public void MoveLeftTest4() 83 | { 84 | var t = new TextEditor 85 | { 86 | AllText = """ 87 | 88 | one two three 89 | 3.14 90 | test.com 91 | 92 | """, 93 | CursorPosition = (1, 3), 94 | }; 95 | 96 | // Moving left when not at the start of the line should just move left. 97 | t.Movement.MoveLeft(); 98 | Assert.AreEqual((1, 2), t.CursorPosition); 99 | Assert.AreEqual((1, 2), t.Selection.Start); 100 | Assert.AreEqual((1, 2), t.Selection.End); 101 | } 102 | 103 | [TestMethod] 104 | public void MoveLeftSelectTest1() 105 | { 106 | var t = new TextEditor 107 | { 108 | AllText = """ 109 | 110 | one two three 111 | 3.14 112 | test.com 113 | 114 | """, 115 | }; 116 | 117 | // Moving left at the start of the document while selecting should be a no-op. 118 | t.Movement.MoveLeft(1, true); 119 | Assert.AreEqual((0, 0), t.CursorPosition); 120 | Assert.AreEqual((0, 0), t.Selection.Start); 121 | Assert.AreEqual((0, 0), t.Selection.End); 122 | } 123 | 124 | [TestMethod] 125 | public void MoveLeftSelectTest2() 126 | { 127 | var t = new TextEditor 128 | { 129 | AllText = """ 130 | 131 | one two three 132 | 3.14 133 | test.com 134 | 135 | """, 136 | CursorPosition = (1, 3), 137 | }; 138 | 139 | t.Movement.MoveLeft(1, true); 140 | Assert.AreEqual((1, 2), t.CursorPosition); 141 | Assert.AreEqual((1, 2), t.Selection.Start); 142 | Assert.AreEqual((1, 3), t.Selection.End); 143 | } 144 | 145 | [TestMethod] 146 | public void MoveLeftSelectTest3() 147 | { 148 | var t = new TextEditor 149 | { 150 | AllText = """ 151 | 152 | one two three 153 | 3.14 154 | test.com 155 | 156 | """, 157 | CursorPosition = (1, 0), 158 | }; 159 | 160 | // Moving left at the start of a line while selecting should extend the selection to the end of the previous line. 161 | t.Movement.MoveLeft(1, true); 162 | Assert.AreEqual((0, 0), t.CursorPosition); 163 | Assert.AreEqual((0, 0), t.Selection.Start); 164 | Assert.AreEqual((1, 0), t.Selection.End); 165 | } 166 | 167 | [TestMethod] 168 | public void MoveLeftByWordTest1() 169 | { 170 | var t = new TextEditor 171 | { 172 | AllText = """ 173 | 174 | one two three 175 | 3.14 176 | test.com 177 | 178 | """, 179 | }; 180 | 181 | // Moving left by a word at the start of the document should be a no-op. 182 | t.Movement.MoveLeft(1, false, true); 183 | Assert.AreEqual((0, 0), t.CursorPosition); 184 | Assert.AreEqual((0, 0), t.Selection.Start); 185 | Assert.AreEqual((0, 0), t.Selection.End); 186 | } 187 | 188 | [TestMethod] 189 | public void MoveLeftSelectByWordTest1() 190 | { 191 | var t = new TextEditor 192 | { 193 | AllText = """ 194 | 195 | one two three 196 | 3.14 197 | test.com 198 | 199 | """, 200 | }; 201 | 202 | // Moving left by a word at the start of the document while selecting should be a no-op. 203 | t.Movement.MoveLeft(1, true, true); 204 | Assert.AreEqual((0, 0), t.CursorPosition); 205 | Assert.AreEqual((0, 0), t.Selection.Start); 206 | Assert.AreEqual((0, 0), t.Selection.End); 207 | } 208 | 209 | [TestMethod] 210 | public void MoveLeftSelectByWordTest2() 211 | { 212 | var t = new TextEditor 213 | { 214 | AllText = """ 215 | 216 | one two three 217 | 3.14 218 | test.com 219 | 220 | """, 221 | Options = { IsColorizerEnabled = false }, 222 | CursorPosition = (1, 3), 223 | }; 224 | 225 | // Moving left by a word while selecting should select the previous word. 226 | t.Movement.MoveLeft(1, true, true); 227 | Assert.AreEqual((1, 0), t.CursorPosition); 228 | Assert.AreEqual((1, 0), t.Selection.Start); 229 | Assert.AreEqual((1, 3), t.Selection.End); 230 | } 231 | 232 | [TestMethod] 233 | public void MoveLeftSelectByWordTest3() 234 | { 235 | var t = new TextEditor 236 | { 237 | AllText = """ 238 | 239 | one two three 240 | 3.14 241 | test.com 242 | 243 | """, 244 | Options = { IsColorizerEnabled = false }, 245 | CursorPosition = (1, 4), 246 | }; 247 | 248 | // Moving left by a word while selecting should select the previous word. 249 | t.Movement.MoveLeft(1, true, true); 250 | Assert.AreEqual((1, 0), t.CursorPosition); 251 | Assert.AreEqual((1, 0), t.Selection.Start); 252 | Assert.AreEqual((1, 4), t.Selection.End); 253 | } 254 | 255 | [TestMethod] 256 | public void MoveRightTest1() 257 | { 258 | var t = new TextEditor 259 | { 260 | AllText = """ 261 | 262 | one two three 263 | 3.14 264 | test.com 265 | 266 | """, 267 | }; 268 | 269 | Assert.AreEqual((0, 0), t.CursorPosition); 270 | Assert.AreEqual((0, 0), t.Selection.Start); 271 | Assert.AreEqual((0, 0), t.Selection.End); 272 | 273 | // Moving right on an empty line should move to the start of the line below 274 | t.Movement.MoveRight(); 275 | Assert.AreEqual((1, 0), t.CursorPosition); 276 | Assert.AreEqual((1, 0), t.Selection.Start); 277 | Assert.AreEqual((1, 0), t.Selection.End); 278 | } 279 | 280 | [TestMethod] 281 | public void MoveRightTest2() 282 | { 283 | var t = new TextEditor 284 | { 285 | AllText = """ 286 | 287 | one two three 288 | 3.14 289 | test.com 290 | 291 | """, 292 | }; 293 | 294 | // Moving right twice on an empty line followed by a non-empty line should go to the next line and then past the first character. 295 | t.Movement.MoveRight(2); 296 | Assert.AreEqual((1, 1), t.CursorPosition); 297 | Assert.AreEqual((1, 1), t.Selection.Start); 298 | Assert.AreEqual((1, 1), t.Selection.End); 299 | } 300 | 301 | [TestMethod] 302 | public void MoveRightTest3() 303 | { 304 | var t = new TextEditor 305 | { 306 | AllText = """ 307 | 308 | one two three 309 | 3.14 310 | test.com 311 | 312 | """, 313 | CursorPosition = (4, 0), 314 | }; 315 | 316 | // Moving right at the end of the document should be a no-op. 317 | t.Movement.MoveRight(); 318 | Assert.AreEqual((4, 0), t.CursorPosition); 319 | Assert.AreEqual((4, 0), t.Selection.Start); 320 | Assert.AreEqual((4, 0), t.Selection.End); 321 | } 322 | 323 | [TestMethod] 324 | public void MoveRightSelectTest1() 325 | { 326 | var t = new TextEditor 327 | { 328 | AllText = """ 329 | 330 | one two three 331 | 3.14 332 | test.com 333 | 334 | """, 335 | }; 336 | 337 | // Moving right on an empty line while selecting should extend the selection to the start of the next line. 338 | t.Movement.MoveRight(1, true); 339 | Assert.AreEqual((1, 0), t.CursorPosition); 340 | Assert.AreEqual((0, 0), t.Selection.Start); 341 | Assert.AreEqual((1, 0), t.Selection.End); 342 | 343 | // Doing it again should extend the selection to include the first char on the following line. 344 | t.Movement.MoveRight(1, true); 345 | Assert.AreEqual((1, 1), t.CursorPosition); 346 | Assert.AreEqual((0, 0), t.Selection.Start); 347 | Assert.AreEqual((1, 1), t.Selection.End); 348 | } 349 | 350 | [TestMethod] 351 | public void MoveRightByWordTest1() 352 | { 353 | var t = new TextEditor 354 | { 355 | AllText = """ 356 | 357 | one two three 358 | 3.14 359 | test.com 360 | 361 | """, 362 | }; 363 | 364 | // Moving right on an empty line while moving by word should move the cursor to the start of the next line. 365 | t.Movement.MoveRight(1, false, true); 366 | Assert.AreEqual((1, 0), t.CursorPosition); 367 | Assert.AreEqual((1, 0), t.Selection.Start); 368 | Assert.AreEqual((1, 0), t.Selection.End); 369 | 370 | // Doing it again should move the cursor past the first word on the following line. 371 | t.Movement.MoveRight(1, false, true); 372 | Assert.AreEqual((1, 4), t.CursorPosition); 373 | Assert.AreEqual((1, 4), t.Selection.Start); 374 | Assert.AreEqual((1, 4), t.Selection.End); 375 | } 376 | 377 | [TestMethod] 378 | public void MoveRightSelectByWordTest1() 379 | { 380 | var t = new TextEditor 381 | { 382 | AllText = """ 383 | 384 | one two three 385 | 3.14 386 | test.com 387 | 388 | """, 389 | Options = { IsColorizerEnabled = false }, 390 | }; 391 | 392 | t.Movement.MoveRight(1, true, true); 393 | Assert.AreEqual((1, 0), t.CursorPosition); 394 | Assert.AreEqual((0, 0), t.Selection.Start); 395 | Assert.AreEqual((1, 0), t.Selection.End); 396 | 397 | t.Movement.MoveRight(1, true, true); 398 | Assert.AreEqual((1, 4), t.CursorPosition); 399 | Assert.AreEqual((0, 0), t.Selection.Start); 400 | Assert.AreEqual((1, 4), t.Selection.End); 401 | } 402 | 403 | [TestMethod] 404 | public void MoveRightTest4() 405 | { 406 | var t = new TextEditor 407 | { 408 | AllText = """ 409 | 410 | one two three 411 | 3.14 412 | test.com 413 | 414 | """, 415 | CursorPosition = (1, 3), 416 | }; 417 | 418 | t.Movement.MoveRight(); 419 | Assert.AreEqual((1, 4), t.CursorPosition); 420 | Assert.AreEqual((1, 4), t.Selection.Start); 421 | Assert.AreEqual((1, 4), t.Selection.End); 422 | } 423 | 424 | [TestMethod] 425 | public void MoveRightSelectTest2() 426 | { 427 | var t = new TextEditor 428 | { 429 | AllText = """ 430 | 431 | one two three 432 | 3.14 433 | test.com 434 | 435 | """, 436 | CursorPosition = (1, 3), 437 | }; 438 | 439 | t.Movement.MoveRight(1, true); 440 | Assert.AreEqual((1, 4), t.CursorPosition); 441 | Assert.AreEqual((1, 3), t.Selection.Start); 442 | Assert.AreEqual((1, 4), t.Selection.End); 443 | } 444 | 445 | [TestMethod] 446 | public void MoveRightSelectByWordTest2() 447 | { 448 | var t = new TextEditor 449 | { 450 | AllText = """ 451 | 452 | one two three 453 | 3.14 454 | test.com 455 | 456 | """, 457 | Options = { IsColorizerEnabled = false }, 458 | CursorPosition = (1, 3), 459 | }; 460 | 461 | t.Movement.MoveRight(1, true, true); 462 | Assert.AreEqual((1, 4), t.CursorPosition); 463 | Assert.AreEqual((1, 0), t.Selection.Start); 464 | Assert.AreEqual((1, 4), t.Selection.End); 465 | } 466 | 467 | [TestMethod] 468 | public void MoveRightSelectByWordTest3() 469 | { 470 | var t = new TextEditor 471 | { 472 | AllText = """ 473 | 474 | one two three 475 | 3.14 476 | test.com 477 | 478 | """, 479 | Options = { IsColorizerEnabled = false }, 480 | CursorPosition = (1, 4), 481 | }; 482 | 483 | t.Movement.MoveRight(1, true, true); 484 | Assert.AreEqual((1, 8), t.CursorPosition); 485 | Assert.AreEqual((1, 4), t.Selection.Start); 486 | Assert.AreEqual((1, 8), t.Selection.End); 487 | } 488 | 489 | // control.MoveLeft(); 490 | // control.MoveRight(); 491 | // control.MoveUp(); 492 | // control.MoveDown(); 493 | // control.MoveTop(); 494 | // control.MoveBottom(); 495 | // control.MoveHome(); 496 | // control.MoveEnd(); 497 | } 498 | -------------------------------------------------------------------------------- /src/TextEdit/Editor/TextEditorText.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace ImGuiColorTextEditNet.Editor; 8 | 9 | internal class TextEditorText 10 | { 11 | readonly TextEditorOptions _options; 12 | readonly List _lines = new(); 13 | 14 | internal long Version { get; private set; } 15 | 16 | internal TextEditorText(TextEditorOptions options) 17 | { 18 | _options = options ?? throw new ArgumentNullException(nameof(options)); 19 | _lines.Add(new Line()); 20 | } 21 | 22 | internal int LineCount => _lines.Count; 23 | internal int? PendingScrollRequest { get; set; } 24 | 25 | internal delegate void LineAddedHandler(int index); 26 | internal delegate void LinesRemovedHandler(int start, int end); 27 | 28 | internal event Action? AllTextReplaced; 29 | internal event LineAddedHandler? LineAdded; 30 | internal event LinesRemovedHandler? LinesRemoved; 31 | 32 | internal ReadOnlySpan GetLine(int index) => 33 | CollectionsMarshal.AsSpan(_lines[index].Glyphs); 34 | 35 | /// This should currently only be used for syntax highlighting, so we don't need to update Version. 36 | internal Span GetMutableLine(int index) => 37 | CollectionsMarshal.AsSpan(_lines[index].Glyphs); 38 | 39 | internal string GetLineText(int index) 40 | { 41 | var line = _lines[index]; 42 | var sb = new StringBuilder(line.Length); 43 | line.GetString(sb); 44 | return sb.ToString(); 45 | } 46 | 47 | internal static bool IsBlank(char c) => c is ' ' or '\t'; 48 | 49 | internal string GetText(Coordinates startPos, Coordinates endPos) 50 | { 51 | var lineNum = startPos.Line; 52 | var maxLineNum = endPos.Line; 53 | var index = GetCharacterIndex(startPos); 54 | var maxIndex = GetCharacterIndex(endPos); 55 | 56 | int bufferSize = 0; 57 | for (int i = lineNum; i < maxLineNum; i++) 58 | bufferSize += _lines[i].Length; 59 | 60 | var result = new StringBuilder(bufferSize + bufferSize / 8); 61 | while (index < maxIndex || lineNum < maxLineNum) 62 | { 63 | if (lineNum >= _lines.Count) 64 | break; 65 | 66 | var line = _lines[lineNum].Glyphs; 67 | if (index < line.Count) 68 | { 69 | result.Append(line[index].Char); 70 | index++; 71 | } 72 | else 73 | { 74 | index = 0; 75 | ++lineNum; 76 | if (lineNum < _lines.Count) 77 | result.Append(Environment.NewLine); 78 | } 79 | } 80 | 81 | return result.ToString(); 82 | } 83 | 84 | internal void SetText(string value) 85 | { 86 | _lines.Clear(); 87 | _lines.Add(new Line()); 88 | 89 | foreach (var chr in value) 90 | { 91 | if (chr == '\r') 92 | { 93 | // ignore the carriage return character 94 | } 95 | else if (chr == '\n') 96 | { 97 | _lines.Add(new Line()); 98 | } 99 | else 100 | { 101 | _lines[^1].Glyphs.Add(new Glyph(chr, PaletteIndex.Default)); 102 | } 103 | } 104 | 105 | Version++; 106 | AllTextReplaced?.Invoke(); 107 | } 108 | 109 | internal IList TextLines 110 | { 111 | get 112 | { 113 | var result = new string[_lines.Count]; 114 | 115 | var sb = new StringBuilder(); 116 | for (int i = 0; i < _lines.Count; i++) 117 | { 118 | var line = _lines[i]; 119 | sb.Clear(); 120 | 121 | for (int j = 0; j < line.Glyphs.Count; ++j) 122 | sb.Append(line.Glyphs[j].Char); 123 | 124 | result[i] = sb.ToString(); 125 | } 126 | 127 | return result; 128 | } 129 | set 130 | { 131 | _lines.Clear(); 132 | 133 | if (value.Count == 0) 134 | { 135 | _lines.Add(new Line()); 136 | } 137 | else 138 | { 139 | _lines.Capacity = value.Count; 140 | foreach (var stringLine in value) 141 | { 142 | var internalLine = new Line(new List(stringLine.Length)); 143 | foreach (var c in stringLine) 144 | internalLine.Glyphs.Add(new Glyph(c, PaletteIndex.Default)); 145 | 146 | _lines.Add(internalLine); 147 | } 148 | } 149 | 150 | Version++; 151 | AllTextReplaced?.Invoke(); 152 | } 153 | } 154 | 155 | internal void DeleteRange(Coordinates startPos, Coordinates endPos) 156 | { 157 | Util.Assert(endPos >= startPos); 158 | Util.Assert(!_options.IsReadOnly); 159 | 160 | // Console.WriteLine($"D({startPos.Line}.{startPos.Column})-({endPos.Line}.{endPos.Column})\n"); 161 | 162 | if (endPos == startPos) 163 | return; 164 | 165 | var start = GetCharacterIndex(startPos); 166 | var end = GetCharacterIndex(endPos); 167 | 168 | if (startPos.Line == endPos.Line) 169 | { 170 | var line = _lines[startPos.Line].Glyphs; 171 | var n = GetLineMaxColumn(startPos.Line); 172 | if (endPos.Column >= n) 173 | line.RemoveRange(start, line.Count - start); 174 | else 175 | line.RemoveRange(start, end - start); 176 | } 177 | else 178 | { 179 | var firstLine = _lines[startPos.Line].Glyphs; 180 | var lastLine = _lines[endPos.Line].Glyphs; 181 | 182 | firstLine.RemoveRange(start, firstLine.Count - start); 183 | lastLine.RemoveRange(0, end); 184 | 185 | if (startPos.Line < endPos.Line) 186 | firstLine.AddRange(lastLine); 187 | 188 | if (startPos.Line < endPos.Line) 189 | RemoveLine(startPos.Line + 1, endPos.Line + 1); 190 | } 191 | 192 | Version++; 193 | } 194 | 195 | void RemoveLine(int start, int end) 196 | { 197 | Util.Assert(!_options.IsReadOnly); 198 | Util.Assert(end >= start); 199 | Util.Assert(_lines.Count > end - start); 200 | 201 | _lines.RemoveRange(start, end - start); 202 | LinesRemoved?.Invoke(start, end); 203 | Util.Assert(_lines.Count != 0); 204 | } 205 | 206 | internal void RemoveLine(int lineNumber) 207 | { 208 | Util.Assert(!_options.IsReadOnly); 209 | Util.Assert(_lines.Count > 1); 210 | 211 | _lines.RemoveAt(lineNumber); 212 | Version++; 213 | LinesRemoved?.Invoke(lineNumber, lineNumber); 214 | Util.Assert(_lines.Count != 0); 215 | } 216 | 217 | internal void RemoveInLine(int lineNum, int start, int end) // Removes range from [start..end), i.e. character at end index is not removed 218 | { 219 | if (end < start) 220 | return; 221 | 222 | var line = _lines[lineNum]; 223 | 224 | if (end > line.Glyphs.Count) 225 | end = line.Glyphs.Count; 226 | 227 | Version++; 228 | line.Glyphs.RemoveRange(start, end - start); 229 | } 230 | 231 | List InsertLine(int lineNumber) 232 | { 233 | Util.Assert(!_options.IsReadOnly); 234 | 235 | var result = new Line(); 236 | _lines.Insert(lineNumber, result); 237 | Version++; 238 | LineAdded?.Invoke(lineNumber); 239 | return result.Glyphs; 240 | } 241 | 242 | internal void InsertLine(int lineNumber, string text, PaletteIndex color = PaletteIndex.Default) 243 | { 244 | var line = new Line(new List(text.Length)); 245 | foreach (var c in text) 246 | line.Glyphs.Add(new Glyph(c, color)); 247 | 248 | Version++; 249 | InsertLine(lineNumber, line); 250 | } 251 | 252 | internal void InsertLine(int lineNumber, Line line) 253 | { 254 | Util.Assert(!_options.IsReadOnly); 255 | _lines.Insert(lineNumber, line); 256 | Version++; 257 | LineAdded?.Invoke(lineNumber); 258 | } 259 | 260 | internal void AppendToLine(int lineNum, string text, PaletteIndex color = PaletteIndex.Default) 261 | { 262 | var line = _lines[lineNum]; 263 | foreach (var c in text) 264 | line.Glyphs.Add(new Glyph(c, color)); 265 | 266 | Version++; 267 | } 268 | 269 | internal void InsertCharAt(Coordinates pos, char c) 270 | { 271 | Util.Assert(!_options.IsReadOnly); 272 | Util.Assert(_lines.Count != 0); 273 | 274 | if (c == '\r') 275 | return; 276 | 277 | int cindex = GetCharacterIndex(pos); 278 | if (c == '\n') 279 | { 280 | if (cindex < _lines[pos.Line].Glyphs.Count) 281 | { 282 | var newLine = InsertLine(pos.Line + 1); 283 | var line = _lines[pos.Line].Glyphs; 284 | newLine.InsertRange(0, line.Skip(cindex)); 285 | line.RemoveRange(cindex, line.Count - cindex); 286 | } 287 | else 288 | { 289 | InsertLine(pos.Line + 1); 290 | } 291 | 292 | pos.Line++; 293 | pos.Column = 0; 294 | } 295 | else 296 | { 297 | var line = _lines[pos.Line].Glyphs; 298 | var glyph = new Glyph(c, PaletteIndex.Default); 299 | line.Insert(cindex, glyph); 300 | pos.Column++; 301 | } 302 | 303 | Version++; 304 | } 305 | 306 | internal Coordinates InsertTextAt(Coordinates pos, string value) 307 | { 308 | Util.Assert(!_options.IsReadOnly); 309 | 310 | int cindex = GetCharacterIndex(pos); 311 | foreach (var c in value) 312 | { 313 | Util.Assert(_lines.Count != 0); 314 | 315 | if (c == '\r') 316 | continue; 317 | 318 | if (c == '\n') 319 | { 320 | if (cindex < _lines[pos.Line].Glyphs.Count) 321 | { 322 | var newLine = InsertLine(pos.Line + 1); 323 | var line = _lines[pos.Line].Glyphs; 324 | newLine.InsertRange(0, line.Skip(cindex)); 325 | line.RemoveRange(cindex, line.Count - cindex); 326 | } 327 | else 328 | { 329 | InsertLine(pos.Line + 1); 330 | } 331 | 332 | pos.Line++; 333 | pos.Column = 0; 334 | cindex = 0; 335 | } 336 | else 337 | { 338 | var line = _lines[pos.Line].Glyphs; 339 | var glyph = new Glyph(c, PaletteIndex.Default); 340 | line.Insert(cindex, glyph); 341 | 342 | cindex++; 343 | pos.Column++; 344 | } 345 | } 346 | 347 | Version++; 348 | return pos; 349 | } 350 | 351 | internal string GetWordAt(Coordinates position) 352 | { 353 | var start = FindWordStart(position); 354 | var end = FindWordEnd(position); 355 | 356 | var sb = new StringBuilder(); 357 | 358 | var istart = GetCharacterIndex(start); 359 | var iend = GetCharacterIndex(end); 360 | 361 | for (var it = istart; it < iend; ++it) 362 | sb.Append(_lines[position.Line].Glyphs[it].Char); 363 | 364 | return sb.ToString(); 365 | } 366 | 367 | internal int GetCharacterIndex(Coordinates position) => 368 | position.Line >= _lines.Count 369 | ? -1 370 | : _lines[position.Line].GetCharacterIndex(position, _options); 371 | 372 | internal int GetCharacterColumn(int lineNumber, int indexInLine) => 373 | lineNumber >= _lines.Count 374 | ? 0 375 | : _lines[lineNumber].GetCharacterColumn(indexInLine, _options); 376 | 377 | internal int GetLineMaxColumn(int lineNumber) => 378 | lineNumber >= _lines.Count ? 0 : _lines[lineNumber].GetLineMaxColumn(_options); 379 | 380 | internal bool IsOnWordBoundary(Coordinates position) 381 | { 382 | if (position.Line >= _lines.Count || position.Column == 0) 383 | return true; 384 | 385 | var line = _lines[position.Line].Glyphs; 386 | var cindex = GetCharacterIndex(position); 387 | 388 | if (cindex >= line.Count) 389 | return true; 390 | 391 | if (_options.IsColorizerEnabled) 392 | return line[cindex].ColorIndex != line[cindex - 1].ColorIndex; 393 | 394 | return char.IsWhiteSpace(line[cindex].Char) != char.IsWhiteSpace(line[cindex - 1].Char); 395 | } 396 | 397 | internal Coordinates SanitizeCoordinates(Coordinates value) 398 | { 399 | var line = value.Line; 400 | var column = value.Column; 401 | if (line < _lines.Count) 402 | { 403 | column = _lines.Count == 0 ? 0 : Math.Min(column, GetLineMaxColumn(line)); 404 | return (line, column); 405 | } 406 | 407 | if (_lines.Count == 0) 408 | { 409 | line = 0; 410 | column = 0; 411 | } 412 | else 413 | { 414 | line = _lines.Count - 1; 415 | column = GetLineMaxColumn(line); 416 | } 417 | 418 | return (line, column); 419 | } 420 | 421 | internal void Advance(Coordinates position) 422 | { 423 | if (position.Line >= _lines.Count) 424 | return; 425 | 426 | var line = _lines[position.Line].Glyphs; 427 | var cindex = GetCharacterIndex(position); 428 | 429 | if (cindex + 1 < line.Count) 430 | { 431 | cindex = Math.Min(cindex + 1, line.Count - 1); 432 | } 433 | else 434 | { 435 | ++position.Line; 436 | cindex = 0; 437 | } 438 | 439 | position.Column = GetCharacterColumn(position.Line, cindex); 440 | } 441 | 442 | internal Coordinates FindWordStart(Coordinates position) 443 | { 444 | if (position.Line >= _lines.Count) 445 | return position; 446 | 447 | var line = _lines[position.Line].Glyphs; 448 | var cindex = GetCharacterIndex(position); 449 | 450 | if (cindex >= line.Count) 451 | return position; 452 | 453 | while (cindex > 0 && char.IsWhiteSpace(line[cindex].Char)) 454 | --cindex; 455 | 456 | var cstart = line[cindex].ColorIndex; 457 | while (cindex > 0) 458 | { 459 | var c = line[cindex].Char; 460 | if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx 461 | { 462 | if (c <= 32 && char.IsWhiteSpace(c)) 463 | { 464 | cindex++; 465 | break; 466 | } 467 | if (cstart != line[cindex - 1].ColorIndex) 468 | break; 469 | } 470 | --cindex; 471 | } 472 | 473 | return (position.Line, GetCharacterColumn(position.Line, cindex)); 474 | } 475 | 476 | internal Coordinates FindWordEnd(Coordinates position) 477 | { 478 | if (position.Line >= _lines.Count) 479 | return position; 480 | 481 | var line = _lines[position.Line].Glyphs; 482 | var cindex = GetCharacterIndex(position); 483 | 484 | if (cindex >= line.Count) 485 | return position; 486 | 487 | bool prevspace = char.IsWhiteSpace(line[cindex].Char); 488 | var cstart = line[cindex].ColorIndex; 489 | while (cindex < line.Count) 490 | { 491 | var c = line[cindex].Char; 492 | if (cstart != line[cindex].ColorIndex) 493 | break; 494 | 495 | if (prevspace != char.IsWhiteSpace(c)) 496 | { 497 | if (char.IsWhiteSpace(c)) 498 | while (cindex < line.Count && char.IsWhiteSpace(line[cindex].Char)) 499 | ++cindex; 500 | break; 501 | } 502 | cindex++; 503 | } 504 | 505 | return (position.Line, GetCharacterColumn(position.Line, cindex)); 506 | } 507 | 508 | internal Coordinates FindNextWord(Coordinates from) 509 | { 510 | Coordinates at = from; 511 | if (at.Line >= _lines.Count) 512 | return at; 513 | 514 | // skip to the next non-word character 515 | var cindex = GetCharacterIndex(from); 516 | bool isword = false; 517 | bool skip = false; 518 | 519 | if (cindex < _lines[at.Line].Glyphs.Count) 520 | { 521 | var line = _lines[at.Line].Glyphs; 522 | isword = char.IsLetterOrDigit(line[cindex].Char); 523 | skip = isword; 524 | } 525 | 526 | while (!isword || skip) 527 | { 528 | if (at.Line >= _lines.Count) 529 | { 530 | var l = Math.Max(0, _lines.Count - 1); 531 | return (l, GetLineMaxColumn(l)); 532 | } 533 | 534 | var line = _lines[at.Line].Glyphs; 535 | if (cindex < line.Count) 536 | { 537 | isword = char.IsLetterOrDigit(line[cindex].Char); 538 | 539 | if (isword && !skip) 540 | return (at.Line, GetCharacterColumn(at.Line, cindex)); 541 | 542 | if (!isword) 543 | skip = false; 544 | 545 | cindex++; 546 | } 547 | else 548 | { 549 | cindex = 0; 550 | ++at.Line; 551 | skip = false; 552 | isword = false; 553 | } 554 | } 555 | 556 | return at; 557 | } 558 | 559 | internal void Append(ReadOnlySpan text, PaletteIndex color) 560 | { 561 | var line = _lines[^1]; 562 | 563 | foreach (var c in text) 564 | { 565 | if (c == '\r') 566 | continue; 567 | 568 | if (c == '\n') 569 | { 570 | line = new Line(); 571 | InsertLine(LineCount, line); 572 | } 573 | else 574 | { 575 | line.Glyphs.Add(new Glyph(c, color)); 576 | } 577 | } 578 | 579 | Version++; 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/TextEdit/Syntax/LanguageDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ImGuiColorTextEditNet.Syntax; 4 | 5 | /// Represents a language definition for syntax highlighting. 6 | public class LanguageDefinition 7 | { 8 | /// The name of the language, used for identification and display purposes. 9 | public string Name; 10 | 11 | /// A list of keywords for the language, used for syntax highlighting. 12 | public string[]? Keywords; 13 | 14 | /// A list of identifiers for the language, which may include built-in functions, types, or other significant terms. 15 | public string[]? Identifiers; 16 | 17 | /// The start and end strings for multi-line comments, used for syntax highlighting. 18 | public string CommentStart = "/*"; 19 | 20 | /// The end string for multi-line comments, used for syntax highlighting. 21 | public string CommentEnd = "*/"; 22 | 23 | /// The string used for single-line comments, used for syntax highlighting. 24 | public string SingleLineComment = "//"; 25 | 26 | /// The character used to denote preprocessor directives, such as `#include` or `#define`. 27 | public char PreprocChar = '#'; 28 | 29 | /// Indicates whether the language supports auto-indentation, which can help with formatting code as it is typed. 30 | public bool AutoIndentation = true; 31 | 32 | /// Indicates whether the language is case-sensitive, affecting how keywords and identifiers are matched during syntax highlighting. 33 | public bool CaseSensitive = true; 34 | 35 | /// A list of regular expressions that define token patterns for syntax highlighting. 36 | public List<(string, PaletteIndex)> TokenRegexStrings = []; // TODO: Actually use this 37 | 38 | /// 39 | /// Initializes a new instance of the class with the specified name. 40 | /// 41 | public LanguageDefinition(string name) => Name = name; 42 | 43 | /// Creates a predefined language definition for HLSL (High-Level Shading Language). 44 | public static LanguageDefinition Hlsl() 45 | { 46 | // csharpier-ignore-start 47 | LanguageDefinition langDef = new("HLSL") 48 | { 49 | Keywords = 50 | [ 51 | "AppendStructuredBuffer", "asm", "asm_fragment", "BlendState", "bool", "break", "Buffer", "ByteAddressBuffer", "case", "cbuffer", "centroid", "class", "column_major", "compile", "compile_fragment", 52 | "CompileShader", "const", "continue", "ComputeShader", "ConsumeStructuredBuffer", "default", "DepthStencilState", "DepthStencilView", "discard", "do", "double", "DomainShader", "dword", "else", 53 | "export", "extern", "false", "float", "for", "fxgroup", "GeometryShader", "groupshared", "half", "Hullshader", "if", "in", "inline", "inout", "InputPatch", "int", "interface", "line", "lineadj", 54 | "linear", "LineStream", "matrix", "min16float", "min10float", "min16int", "min12int", "min16uint", "namespace", "nointerpolation", "noperspective", "NULL", "out", "OutputPatch", "packoffset", 55 | "pass", "pixelfragment", "PixelShader", "point", "PointStream", "precise", "RasterizerState", "RenderTargetView", "return", "register", "row_major", "RWBuffer", "RWByteAddressBuffer", "RWStructuredBuffer", 56 | "RWTexture1D", "RWTexture1DArray", "RWTexture2D", "RWTexture2DArray", "RWTexture3D", "sample", "sampler", "SamplerState", "SamplerComparisonState", "shared", "snorm", "stateblock", "stateblock_state", 57 | "static", "string", "struct", "switch", "StructuredBuffer", "tbuffer", "technique", "technique10", "technique11", "texture", "Texture1D", "Texture1DArray", "Texture2D", "Texture2DArray", "Texture2DMS", 58 | "Texture2DMSArray", "Texture3D", "TextureCube", "TextureCubeArray", "true", "typedef", "triangle", "triangleadj", "TriangleStream", "uint", "uniform", "unorm", "unsigned", "vector", "vertexfragment", 59 | "VertexShader", "void", "volatile", "while", 60 | "bool1","bool2","bool3","bool4","double1","double2","double3","double4", "float1", "float2", "float3", "float4", "int1", "int2", "int3", "int4", "in", "out", "inout", 61 | "uint1", "uint2", "uint3", "uint4", "dword1", "dword2", "dword3", "dword4", "half1", "half2", "half3", "half4", 62 | "float1x1","float2x1","float3x1","float4x1","float1x2","float2x2","float3x2","float4x2", 63 | "float1x3","float2x3","float3x3","float4x3","float1x4","float2x4","float3x4","float4x4", 64 | "half1x1","half2x1","half3x1","half4x1","half1x2","half2x2","half3x2","half4x2", 65 | "half1x3","half2x3","half3x3","half4x3","half1x4","half2x4","half3x4","half4x4" 66 | ], 67 | Identifiers = 68 | [ 69 | "abort", "abs", "acos", "all", "AllMemoryBarrier", "AllMemoryBarrierWithGroupSync", "any", "asdouble", "asfloat", 70 | "asin", "asint", "asuint", "atan", "atan2", "ceil", "CheckAccessFullyMapped", "clamp", "clip", "cos", "cosh", 71 | "countbits", "cross", "D3DCOLORtoUBYTE4", "ddx", "ddx_coarse", "ddx_fine", "ddy", "ddy_coarse", "ddy_fine", 72 | "degrees", "determinant", "DeviceMemoryBarrier", "DeviceMemoryBarrierWithGroupSync", "distance", "dot", "dst", 73 | "errorf", "EvaluateAttributeAtCentroid", "EvaluateAttributeAtSample", "EvaluateAttributeSnapped", "exp", "exp2", 74 | "f16tof32", "f32tof16", "faceforward", "firstbithigh", "firstbitlow", "floor", "fma", "fmod", "frac", "frexp", 75 | "fwidth", "GetRenderTargetSampleCount", "GetRenderTargetSamplePosition", "GroupMemoryBarrier", 76 | "GroupMemoryBarrierWithGroupSync", "InterlockedAdd", "InterlockedAnd", "InterlockedCompareExchange", 77 | "InterlockedCompareStore", "InterlockedExchange", "InterlockedMax", "InterlockedMin", "InterlockedOr", 78 | "InterlockedXor", "isfinite", "isinf", "isnan", "ldexp", "length", "lerp", "lit", "log", "log10", "log2", "mad", 79 | "max", "min", "modf", "msad4", "mul", "noise", "normalize", "pow", "printf", "Process2DQuadTessFactorsAvg", 80 | "Process2DQuadTessFactorsMax", "Process2DQuadTessFactorsMin", "ProcessIsolineTessFactors", 81 | "ProcessQuadTessFactorsAvg", "ProcessQuadTessFactorsMax", "ProcessQuadTessFactorsMin", "ProcessTriTessFactorsAvg", 82 | "ProcessTriTessFactorsMax", "ProcessTriTessFactorsMin", "radians", "rcp", "reflect", "refract", "reversebits", 83 | "round", "rsqrt", "saturate", "sign", "sin", "sincos", "sinh", "smoothstep", "sqrt", "step", "tan", "tanh", 84 | "tex1D", "tex1Dbias", "tex1Dgrad", "tex1Dlod", "tex1Dproj", "tex2D", "tex2Dbias", "tex2Dgrad", 85 | "tex2Dlod", "tex2Dproj", "tex3D", "tex3Dbias", "tex3Dgrad", "tex3Dlod", "tex3Dproj", "texCUBE", 86 | "texCUBEbias", "texCUBEgrad", "texCUBElod", "texCUBEproj", "transpose", "trunc" 87 | ] 88 | }; 89 | 90 | langDef.TokenRegexStrings.Add((@"[ \t]*#[ \t]*[a-zA-Z_]+", PaletteIndex.Preprocessor)); 91 | langDef.TokenRegexStrings.Add((@"L?\""(\\.|[^\""])*\""", PaletteIndex.String)); 92 | langDef.TokenRegexStrings.Add((@"\'\\?[^\']\'", PaletteIndex.CharLiteral)); 93 | langDef.TokenRegexStrings.Add(("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex.Number)); 94 | langDef.TokenRegexStrings.Add(("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 95 | langDef.TokenRegexStrings.Add(("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 96 | langDef.TokenRegexStrings.Add(("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex.Number)); 97 | langDef.TokenRegexStrings.Add(("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex.Identifier)); 98 | langDef.TokenRegexStrings.Add((@"[\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.]", PaletteIndex.Punctuation)); 99 | return langDef; 100 | // csharpier-ignore-end 101 | } 102 | 103 | /// Creates a predefined language definition for GLSL (OpenGL Shading Language). 104 | public static LanguageDefinition Glsl() 105 | { 106 | // csharpier-ignore-start 107 | LanguageDefinition langDef = new("GLSL") 108 | { 109 | Keywords = 110 | [ 111 | "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", 112 | "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", 113 | "_Noreturn", "_Static_assert", "_Thread_local" 114 | ], 115 | Identifiers = 116 | [ 117 | "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", 118 | "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" 119 | ] 120 | }; 121 | 122 | langDef.TokenRegexStrings.Add((@"[ \t]*#[ \t]*[a-zA-Z_]+", PaletteIndex.Preprocessor)); 123 | langDef.TokenRegexStrings.Add((@"L?\""(\\.|[^\""])*\""", PaletteIndex.String)); 124 | langDef.TokenRegexStrings.Add((@"\'\\?[^\']\'", PaletteIndex.CharLiteral)); 125 | langDef.TokenRegexStrings.Add(("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex.Number)); 126 | langDef.TokenRegexStrings.Add(("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 127 | langDef.TokenRegexStrings.Add(("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 128 | langDef.TokenRegexStrings.Add(("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex.Number)); 129 | langDef.TokenRegexStrings.Add(("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex.Identifier)); 130 | langDef.TokenRegexStrings.Add((@"[\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.]", PaletteIndex.Punctuation)); 131 | 132 | return langDef; 133 | // csharpier-ignore-end 134 | } 135 | 136 | /// Creates a predefined language definition for SQL (Structured Query Language). 137 | public static LanguageDefinition Sql() 138 | { 139 | // csharpier-ignore-start 140 | LanguageDefinition langDef = new("SQL") 141 | { 142 | CaseSensitive = false, 143 | AutoIndentation = false, 144 | Keywords = 145 | [ 146 | "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", 147 | "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", 148 | "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", 149 | "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", 150 | "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", 151 | "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", 152 | "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", 153 | "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", 154 | "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING","DROP", "OPENROWSET", "VIEW", 155 | "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT" 156 | ], 157 | Identifiers = 158 | [ 159 | "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", 160 | "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", 161 | "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", 162 | "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", 163 | "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", 164 | "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", 165 | "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", 166 | "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", 167 | "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", 168 | "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE " 169 | ] 170 | }; 171 | 172 | langDef.TokenRegexStrings.Add((@"L?\""(\\.|[^\""])*\""", PaletteIndex.String)); 173 | langDef.TokenRegexStrings.Add((@"\'[^\']*\'", PaletteIndex.String)); 174 | langDef.TokenRegexStrings.Add(("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex.Number)); 175 | langDef.TokenRegexStrings.Add(("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 176 | langDef.TokenRegexStrings.Add(("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 177 | langDef.TokenRegexStrings.Add(("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex.Number)); 178 | langDef.TokenRegexStrings.Add(("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex.Identifier)); 179 | langDef.TokenRegexStrings.Add((@"[\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.]", PaletteIndex.Punctuation)); 180 | 181 | return langDef; 182 | // csharpier-ignore-end 183 | } 184 | 185 | /// Creates a predefined language definition for Lua 186 | public static LanguageDefinition Lua() 187 | { 188 | // csharpier-ignore-start 189 | LanguageDefinition langDef = new("Lua") 190 | { 191 | CommentStart = "--[[", 192 | CommentEnd = "]]", 193 | SingleLineComment = "--", 194 | CaseSensitive = true, 195 | AutoIndentation = false, 196 | Keywords = 197 | [ 198 | "and", "break", "do", "", "else", "elseif", "end", "false", "for", "function", "if", "in", "", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while" 199 | ], 200 | Identifiers = 201 | [ 202 | "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", 203 | "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION","arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", 204 | "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug","getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", 205 | "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", 206 | "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", 207 | "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", 208 | "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", 209 | "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", 210 | "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", 211 | "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" 212 | ] 213 | }; 214 | 215 | langDef.TokenRegexStrings.Add((@"L?\""(\\.|[^\""])*\""", PaletteIndex.String)); 216 | langDef.TokenRegexStrings.Add((@"\'[^\']*\'", PaletteIndex.String)); 217 | langDef.TokenRegexStrings.Add(("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex.Number)); 218 | langDef.TokenRegexStrings.Add(("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex.Number)); 219 | langDef.TokenRegexStrings.Add(("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex.Number)); 220 | langDef.TokenRegexStrings.Add(("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex.Identifier)); 221 | langDef.TokenRegexStrings.Add((@"[\[\]\{\}\!\%\^\&\*\(\)\-\+\=\~\|\<\>\?\/\;\,\.]", PaletteIndex.Punctuation)); 222 | 223 | return langDef; 224 | // csharpier-ignore-end 225 | } 226 | } 227 | --------------------------------------------------------------------------------