Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 483ff18b0d | |||
| cd33e8feb1 | |||
| 0bfe75fbf0 | |||
| d15b241140 | |||
| ceef6af3ae | |||
| e62cf3ee28 | |||
| 1a77f28ce4 | |||
| 4d84b352eb | |||
| 1892075d82 | |||
| 719862c842 | |||
| 655cc40162 | |||
| d98785e825 | |||
| 970a31e0d9 | |||
| 464ad8d1ae | |||
| 0cb7d36f2a | |||
| 09a6df0c33 | |||
| 69457c424c | |||
| 24c8040d8a | |||
| e869249a7c | |||
| 35e957b326 | |||
| e7eb35626c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
!.idea
|
||||
cmake-build*
|
||||
build
|
||||
/imgui.ini
|
||||
result
|
||||
|
||||
320
.idea/workspace.xml
generated
320
.idea/workspace.xml
generated
@@ -1,320 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="BackendCodeEditorMiscSettings">
|
||||
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
|
||||
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
|
||||
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
|
||||
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
|
||||
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
|
||||
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
|
||||
</component>
|
||||
<component name="CMakePresetLoader">{
|
||||
"useNewFormat": true
|
||||
}</component>
|
||||
<component name="CMakeProjectFlavorService">
|
||||
<option name="flavorId" value="CMakePlainProjectFlavor" />
|
||||
</component>
|
||||
<component name="CMakeReloadState">
|
||||
<option name="reloaded" value="true" />
|
||||
</component>
|
||||
<component name="CMakeRunConfigurationManager">
|
||||
<generated>
|
||||
<config projectName="kte" targetName="kte" />
|
||||
<config projectName="kte" targetName="imgui" />
|
||||
<config projectName="kte" targetName="kge" />
|
||||
</generated>
|
||||
</component>
|
||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON -DBUILD_GUI:BOOL=ON" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Actually add the screenshot.">
|
||||
<change beforePath="$PROJECT_DIR$/.github/workflows/release.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.github/workflows/release.yml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ClangdSettings">
|
||||
<option name="formatViaClangd" value="false" />
|
||||
</component>
|
||||
<component name="ExecutionTargetManager" SELECTED_TARGET="CMakeBuildProfile:Debug" />
|
||||
<component name="FormatOnSaveOptions">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="PUSH_TAGS">
|
||||
<GitPushTagMode>
|
||||
<option name="argument" value="--tags" />
|
||||
<option name="title" value="All" />
|
||||
</GitPushTagMode>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
<option name="UPDATE_TYPE" value="REBASE" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="OptimizeOnSaveOptions">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
<component name="ProjectApplicationVersion">
|
||||
<option name="ide" value="CLion" />
|
||||
<option name="majorVersion" value="2025" />
|
||||
<option name="minorVersion" value="2.5" />
|
||||
<option name="productBranch" value="Classic" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 3
|
||||
}</component>
|
||||
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
|
||||
<component name="ProjectLevelVcsManager">
|
||||
<OptionsSetting value="false" id="Update" />
|
||||
</component>
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
<option name="sortByType" value="true" />
|
||||
<option name="sortKey" value="BY_TYPE" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"CMake Application.kge.executor": "Run",
|
||||
"CMake Application.test_example.executor": "Run",
|
||||
"CMake Application.test_undo.executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
||||
"NIXITCH_NIX_CONF_DIR": "",
|
||||
"NIXITCH_NIX_OTHER_STORES": "",
|
||||
"NIXITCH_NIX_PATH": "",
|
||||
"NIXITCH_NIX_PROFILES": "",
|
||||
"NIXITCH_NIX_REMOTE": "",
|
||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
||||
"RunOnceActivity.RadMigrateCodeStyle": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||
"RunOnceActivity.west.config.association.type.startup.service": "true",
|
||||
"cf.first.check.clang-format": "false",
|
||||
"cidr.known.project.marker": "true",
|
||||
"code.cleanup.on.save": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
||||
"rearrange.code.on.save": "true",
|
||||
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/docs" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager" selected="CMake Application.kge">
|
||||
<configuration default="true" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
||||
<method v="2">
|
||||
<option name="CLION.EXTERNAL.BUILD" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="CMake Application.imgui" />
|
||||
<item itemvalue="CMake Application.kge" />
|
||||
<item itemvalue="CMake Application.kte" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="" />
|
||||
<created>1764457173148</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="46950000" />
|
||||
<workItem from="1764538560497" duration="215000" />
|
||||
<workItem from="1764539255906" duration="196000" />
|
||||
<workItem from="1764539459951" duration="64000" />
|
||||
<workItem from="1764539535105" duration="10000" />
|
||||
<workItem from="1764539556448" duration="156000" />
|
||||
<workItem from="1764539725338" duration="1075000" />
|
||||
<workItem from="1764542392763" duration="3512000" />
|
||||
<workItem from="1764548345516" duration="9962000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764485311566</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764485311566</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764486011231</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764486011231</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="Handle end-of-file newline semantics and improve scroll alignment logic.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764486876984</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764486876984</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764489870957</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764489870957</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764496151303</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764496151303</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764500200942</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764500200942</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764501532446</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764501532446</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764502480274</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764502480274</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764505723411</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764505723411</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764550164829</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764550164829</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764551986561</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764551986561</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764556512864</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764556512864</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764556854788</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764556854788</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="Actually add the screenshot.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764557759844</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764557759844</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="15" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VCPKGProject">
|
||||
<isAutomaticCheckingOnLaunch value="false" />
|
||||
<isAutomaticFoundErrors value="true" />
|
||||
<isAutomaticReloadCMake value="true" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="Refactoring" />
|
||||
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
|
||||
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
||||
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
||||
<MESSAGE value="Enable installation targets." />
|
||||
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
||||
<MESSAGE value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
|
||||
<MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
|
||||
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`." />
|
||||
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built." />
|
||||
<MESSAGE value="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling." />
|
||||
<MESSAGE value="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines." />
|
||||
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code." />
|
||||
<MESSAGE value="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup." />
|
||||
<MESSAGE value="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0." />
|
||||
<MESSAGE value="Actually add the screenshot." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Actually add the screenshot." />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
<select />
|
||||
</component>
|
||||
</project>
|
||||
66
Buffer.cc
66
Buffer.cc
@@ -6,6 +6,9 @@
|
||||
#include "Buffer.h"
|
||||
#include "UndoSystem.h"
|
||||
#include "UndoTree.h"
|
||||
// For reconstructing highlighter state on copies
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
Buffer::Buffer()
|
||||
@@ -36,12 +39,36 @@ Buffer::Buffer(const Buffer &other)
|
||||
filename_ = other.filename_;
|
||||
is_file_backed_ = other.is_file_backed_;
|
||||
dirty_ = other.dirty_;
|
||||
read_only_ = other.read_only_;
|
||||
mark_set_ = other.mark_set_;
|
||||
mark_curx_ = other.mark_curx_;
|
||||
mark_cury_ = other.mark_cury_;
|
||||
// Copy syntax/highlighting flags
|
||||
version_ = other.version_;
|
||||
syntax_enabled_ = other.syntax_enabled_;
|
||||
filetype_ = other.filetype_;
|
||||
// Fresh undo system for the copy
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
|
||||
// Recreate a highlighter engine for this copy based on filetype/syntax state
|
||||
if (syntax_enabled_) {
|
||||
// Allocate engine and install an appropriate highlighter
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
if (!filetype_.empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
|
||||
if (hl) {
|
||||
highlighter_->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
|
||||
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
// No filetype -> keep syntax enabled but use NullHighlighter
|
||||
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
// Fresh engine has empty caches; nothing to invalidate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,12 +87,32 @@ Buffer::operator=(const Buffer &other)
|
||||
filename_ = other.filename_;
|
||||
is_file_backed_ = other.is_file_backed_;
|
||||
dirty_ = other.dirty_;
|
||||
read_only_ = other.read_only_;
|
||||
mark_set_ = other.mark_set_;
|
||||
mark_curx_ = other.mark_curx_;
|
||||
mark_cury_ = other.mark_cury_;
|
||||
version_ = other.version_;
|
||||
syntax_enabled_ = other.syntax_enabled_;
|
||||
filetype_ = other.filetype_;
|
||||
// Recreate undo system for this instance
|
||||
undo_tree_ = std::make_unique<UndoTree>();
|
||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||
|
||||
// Recreate highlighter engine consistent with syntax settings
|
||||
highlighter_.reset();
|
||||
if (syntax_enabled_) {
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
if (!filetype_.empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
|
||||
if (hl) {
|
||||
highlighter_->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -82,12 +129,18 @@ Buffer::Buffer(Buffer &&other) noexcept
|
||||
filename_(std::move(other.filename_)),
|
||||
is_file_backed_(other.is_file_backed_),
|
||||
dirty_(other.dirty_),
|
||||
read_only_(other.read_only_),
|
||||
mark_set_(other.mark_set_),
|
||||
mark_curx_(other.mark_curx_),
|
||||
mark_cury_(other.mark_cury_),
|
||||
undo_tree_(std::move(other.undo_tree_)),
|
||||
undo_sys_(std::move(other.undo_sys_))
|
||||
{
|
||||
// Move syntax/highlighting state
|
||||
version_ = other.version_;
|
||||
syntax_enabled_ = other.syntax_enabled_;
|
||||
filetype_ = std::move(other.filetype_);
|
||||
highlighter_ = std::move(other.highlighter_);
|
||||
// Update UndoSystem's buffer reference to point to this object
|
||||
if (undo_sys_) {
|
||||
undo_sys_->UpdateBufferReference(*this);
|
||||
@@ -112,12 +165,19 @@ Buffer::operator=(Buffer &&other) noexcept
|
||||
filename_ = std::move(other.filename_);
|
||||
is_file_backed_ = other.is_file_backed_;
|
||||
dirty_ = other.dirty_;
|
||||
read_only_ = other.read_only_;
|
||||
mark_set_ = other.mark_set_;
|
||||
mark_curx_ = other.mark_curx_;
|
||||
mark_cury_ = other.mark_cury_;
|
||||
undo_tree_ = std::move(other.undo_tree_);
|
||||
undo_sys_ = std::move(other.undo_sys_);
|
||||
|
||||
// Move syntax/highlighting state
|
||||
version_ = other.version_;
|
||||
syntax_enabled_ = other.syntax_enabled_;
|
||||
filetype_ = std::move(other.filetype_);
|
||||
highlighter_ = std::move(other.highlighter_);
|
||||
|
||||
// Update UndoSystem's buffer reference to point to this object
|
||||
if (undo_sys_) {
|
||||
undo_sys_->UpdateBufferReference(*this);
|
||||
@@ -366,7 +426,7 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
||||
// Split line at x
|
||||
std::string tail = rows_[y].substr(x);
|
||||
rows_[y].erase(x);
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||
y += 1;
|
||||
x = 0;
|
||||
remain.erase(0, pos + 1);
|
||||
@@ -427,7 +487,7 @@ Buffer::split_line(int row, const int col)
|
||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||
const auto tail = rows_[y].substr(x);
|
||||
rows_[y].erase(x);
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||
}
|
||||
|
||||
|
||||
@@ -455,7 +515,7 @@ Buffer::insert_row(int row, const std::string_view text)
|
||||
row = 0;
|
||||
if (static_cast<std::size_t>(row) > rows_.size())
|
||||
row = static_cast<int>(rows_.size());
|
||||
rows_.insert(rows_.begin() + row, std::string(text));
|
||||
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
134
Buffer.h
134
Buffer.h
@@ -12,6 +12,10 @@
|
||||
|
||||
#include "AppendBuffer.h"
|
||||
#include "UndoSystem.h"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "syntax/HighlighterEngine.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
|
||||
class Buffer {
|
||||
@@ -77,13 +81,13 @@ public:
|
||||
Line() = default;
|
||||
|
||||
|
||||
Line(const char *s)
|
||||
explicit Line(const char *s)
|
||||
{
|
||||
assign_from(s ? std::string(s) : std::string());
|
||||
}
|
||||
|
||||
|
||||
Line(const std::string &s)
|
||||
explicit Line(const std::string &s)
|
||||
{
|
||||
assign_from(s);
|
||||
}
|
||||
@@ -139,29 +143,38 @@ public:
|
||||
|
||||
|
||||
// conversions
|
||||
operator std::string() const
|
||||
explicit operator std::string() const
|
||||
{
|
||||
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size());
|
||||
return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
|
||||
}
|
||||
|
||||
|
||||
// string-like API used by command/renderer layers (implemented via materialization for now)
|
||||
std::string substr(std::size_t pos) const
|
||||
[[nodiscard]] std::string substr(std::size_t pos) const
|
||||
{
|
||||
const std::size_t n = buf_.Size();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
return std::string(buf_.Data() + pos, n - pos);
|
||||
return {};
|
||||
return {buf_.Data() + pos, n - pos};
|
||||
}
|
||||
|
||||
|
||||
std::string substr(std::size_t pos, std::size_t len) const
|
||||
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
|
||||
{
|
||||
const std::size_t n = buf_.Size();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
return {};
|
||||
const std::size_t take = (pos + len > n) ? (n - pos) : len;
|
||||
return std::string(buf_.Data() + pos, take);
|
||||
return {buf_.Data() + pos, take};
|
||||
}
|
||||
|
||||
|
||||
// minimal find() to support search within a line
|
||||
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
|
||||
{
|
||||
// Materialize to std::string for now; Line is backed by AppendBuffer
|
||||
const auto s = static_cast<std::string>(*this);
|
||||
return s.find(needle, pos);
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +267,15 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
||||
// This does not mark the buffer as file-backed.
|
||||
void SetVirtualName(const std::string &name)
|
||||
{
|
||||
filename_ = name;
|
||||
is_file_backed_ = false;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool IsFileBacked() const
|
||||
{
|
||||
return is_file_backed_;
|
||||
@@ -266,20 +288,39 @@ public:
|
||||
}
|
||||
|
||||
|
||||
void SetCursor(std::size_t x, std::size_t y)
|
||||
// Read-only flag
|
||||
[[nodiscard]] bool IsReadOnly() const
|
||||
{
|
||||
return read_only_;
|
||||
}
|
||||
|
||||
|
||||
void SetReadOnly(bool ro)
|
||||
{
|
||||
read_only_ = ro;
|
||||
}
|
||||
|
||||
|
||||
void ToggleReadOnly()
|
||||
{
|
||||
read_only_ = !read_only_;
|
||||
}
|
||||
|
||||
|
||||
void SetCursor(const std::size_t x, const std::size_t y)
|
||||
{
|
||||
curx_ = x;
|
||||
cury_ = y;
|
||||
}
|
||||
|
||||
|
||||
void SetRenderX(std::size_t rx)
|
||||
void SetRenderX(const std::size_t rx)
|
||||
{
|
||||
rx_ = rx;
|
||||
}
|
||||
|
||||
|
||||
void SetOffsets(std::size_t row, std::size_t col)
|
||||
void SetOffsets(const std::size_t row, const std::size_t col)
|
||||
{
|
||||
rowoffs_ = row;
|
||||
coloffs_ = col;
|
||||
@@ -289,6 +330,12 @@ public:
|
||||
void SetDirty(bool d)
|
||||
{
|
||||
dirty_ = d;
|
||||
if (d) {
|
||||
++version_;
|
||||
if (highlighter_) {
|
||||
highlighter_->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +346,7 @@ public:
|
||||
}
|
||||
|
||||
|
||||
void SetMark(std::size_t x, std::size_t y)
|
||||
void SetMark(const std::size_t x, const std::size_t y)
|
||||
{
|
||||
mark_set_ = true;
|
||||
mark_curx_ = x;
|
||||
@@ -327,6 +374,56 @@ public:
|
||||
|
||||
[[nodiscard]] std::string AsString() const;
|
||||
|
||||
// Syntax highlighting integration (per-buffer)
|
||||
[[nodiscard]] std::uint64_t Version() const
|
||||
{
|
||||
return version_;
|
||||
}
|
||||
|
||||
|
||||
void SetSyntaxEnabled(bool on)
|
||||
{
|
||||
syntax_enabled_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool SyntaxEnabled() const
|
||||
{
|
||||
return syntax_enabled_;
|
||||
}
|
||||
|
||||
|
||||
void SetFiletype(const std::string &ft)
|
||||
{
|
||||
filetype_ = ft;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] const std::string &Filetype() const
|
||||
{
|
||||
return filetype_;
|
||||
}
|
||||
|
||||
|
||||
kte::HighlighterEngine *Highlighter()
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
|
||||
const kte::HighlighterEngine *Highlighter() const
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
|
||||
void EnsureHighlighter()
|
||||
{
|
||||
if (!highlighter_)
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
}
|
||||
|
||||
|
||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||
void insert_text(int row, int col, std::string_view text);
|
||||
@@ -344,7 +441,7 @@ public:
|
||||
// Undo system accessors (created per-buffer)
|
||||
UndoSystem *Undo();
|
||||
|
||||
const UndoSystem *Undo() const;
|
||||
[[nodiscard]] const UndoSystem *Undo() const;
|
||||
|
||||
private:
|
||||
// State mirroring original C struct (without undo_tree)
|
||||
@@ -356,12 +453,19 @@ private:
|
||||
std::string filename_;
|
||||
bool is_file_backed_ = false;
|
||||
bool dirty_ = false;
|
||||
bool read_only_ = false;
|
||||
bool mark_set_ = false;
|
||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||
|
||||
// Per-buffer undo state
|
||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||
std::unique_ptr<UndoSystem> undo_sys_;
|
||||
|
||||
// Syntax/highlighting state
|
||||
std::uint64_t version_ = 0; // increment on edits
|
||||
bool syntax_enabled_ = true;
|
||||
std::string filetype_;
|
||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||
};
|
||||
|
||||
#endif // KTE_BUFFER_H
|
||||
|
||||
105
CMakeLists.txt
105
CMakeLists.txt
@@ -4,14 +4,16 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(KTE_VERSION "1.0.3")
|
||||
set(KTE_VERSION "1.2.2")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
set(BUILD_GUI OFF CACHE BOOL "Enable building the graphical version.")
|
||||
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||
|
||||
if (CMAKE_HOST_UNIX)
|
||||
message(STATUS "Build system is POSIX.")
|
||||
@@ -36,6 +38,9 @@ else ()
|
||||
endif ()
|
||||
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
|
||||
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
add_compile_definitions(KTE_ENABLE_TREESITTER)
|
||||
endif ()
|
||||
|
||||
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
|
||||
|
||||
@@ -49,12 +54,36 @@ set(CURSES_NEED_WIDE)
|
||||
find_package(Curses REQUIRED)
|
||||
include_directories(${CURSES_INCLUDE_DIR})
|
||||
|
||||
set(SYNTAX_SOURCES
|
||||
syntax/GoHighlighter.cc
|
||||
syntax/CppHighlighter.cc
|
||||
syntax/JsonHighlighter.cc
|
||||
syntax/ErlangHighlighter.cc
|
||||
syntax/MarkdownHighlighter.cc
|
||||
syntax/TreeSitterHighlighter.cc
|
||||
syntax/LispHighlighter.cc
|
||||
syntax/HighlighterEngine.cc
|
||||
syntax/RustHighlighter.cc
|
||||
syntax/HighlighterRegistry.cc
|
||||
syntax/SqlHighlighter.cc
|
||||
syntax/NullHighlighter.cc
|
||||
syntax/ForthHighlighter.cc
|
||||
syntax/PythonHighlighter.cc
|
||||
syntax/ShellHighlighter.cc
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND SYNTAX_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
endif ()
|
||||
|
||||
set(COMMON_SOURCES
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
Buffer.cc
|
||||
Editor.cc
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
@@ -65,6 +94,41 @@ set(COMMON_SOURCES
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
|
||||
${SYNTAX_SOURCES}
|
||||
)
|
||||
|
||||
|
||||
set(SYNTAX_HEADERS
|
||||
syntax/GoHighlighter.h
|
||||
syntax/HighlighterEngine.h
|
||||
syntax/ShellHighlighter.h
|
||||
syntax/MarkdownHighlighter.h
|
||||
syntax/LispHighlighter.h
|
||||
syntax/SqlHighlighter.h
|
||||
syntax/ForthHighlighter.h
|
||||
syntax/JsonHighlighter.h
|
||||
syntax/TreeSitterHighlighter.h
|
||||
syntax/NullHighlighter.h
|
||||
syntax/CppHighlighter.h
|
||||
syntax/ErlangHighlighter.h
|
||||
syntax/LanguageHighlighter.h
|
||||
syntax/RustHighlighter.h
|
||||
syntax/PythonHighlighter.h
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND THEME_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
endif ()
|
||||
|
||||
set(THEME_HEADERS
|
||||
themes/ThemeHelpers.h
|
||||
themes/EInk.h
|
||||
themes/Gruvbox.h
|
||||
themes/Solarized.h
|
||||
themes/Plan9.h
|
||||
themes/Nord.h
|
||||
)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
@@ -74,6 +138,7 @@ set(COMMON_HEADERS
|
||||
Editor.h
|
||||
AppendBuffer.h
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
@@ -87,6 +152,10 @@ set(COMMON_HEADERS
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
|
||||
${SYNTAX_HEADERS}
|
||||
${THEME_HEADERS}
|
||||
)
|
||||
|
||||
# kte (terminal-first) executable
|
||||
@@ -99,9 +168,24 @@ add_executable(kte
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
# Users can provide their own tree-sitter include/lib via cache variables
|
||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
@@ -121,8 +205,20 @@ if (BUILD_TESTS)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (${BUILD_GUI})
|
||||
@@ -153,6 +249,9 @@ if (${BUILD_GUI})
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
@@ -201,5 +300,5 @@ if (${BUILD_GUI})
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
endif ()
|
||||
|
||||
1303
Command.cc
1303
Command.cc
File diff suppressed because it is too large
Load Diff
26
Command.h
26
Command.h
@@ -23,8 +23,10 @@ enum class CommandId {
|
||||
Refresh, // force redraw
|
||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||
FindStart, // begin incremental search (placeholder)
|
||||
RegexFindStart, // begin regex search (C-r)
|
||||
RegexpReplace, // begin regex search & replace (C-t)
|
||||
SearchReplace, // begin search & replace (two-step prompt)
|
||||
OpenFileStart, // begin open-file prompt
|
||||
// GUI: visual file picker
|
||||
VisualFilePickerToggle,
|
||||
// Buffers
|
||||
BufferSwitchStart, // begin buffer switch prompt
|
||||
@@ -56,6 +58,8 @@ enum class CommandId {
|
||||
MoveEnd,
|
||||
PageUp,
|
||||
PageDown,
|
||||
ScrollUp, // scroll viewport up (towards beginning) without moving cursor
|
||||
ScrollDown, // scroll viewport down (towards end) without moving cursor
|
||||
WordPrev,
|
||||
WordNext,
|
||||
DeleteWordPrev, // delete previous word (ESC BACKSPACE)
|
||||
@@ -67,10 +71,15 @@ enum class CommandId {
|
||||
Redo,
|
||||
// UI/status helpers
|
||||
UArgStatus, // update status line during universal-argument collection
|
||||
// Themes (GUI)
|
||||
ThemeNext,
|
||||
ThemePrev,
|
||||
// Region formatting
|
||||
IndentRegion, // indent region (C-k =)
|
||||
UnindentRegion, // unindent region (C-k -)
|
||||
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
||||
// Read-only buffers
|
||||
ToggleReadOnly, // toggle current buffer read-only (C-k ')
|
||||
// Buffer operations
|
||||
ReloadBuffer, // reload buffer from disk (C-k l)
|
||||
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
||||
@@ -78,8 +87,19 @@ enum class CommandId {
|
||||
JumpToLine, // prompt for line and jump (C-k g)
|
||||
ShowWorkingDirectory, // Display the current working directory in the editor message.
|
||||
ChangeWorkingDirectory, // Change the editor's current directory.
|
||||
// Help
|
||||
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
// Generic command prompt
|
||||
CommandPromptStart, // begin generic command prompt (C-k ;)
|
||||
// Theme by name
|
||||
ThemeSetByName,
|
||||
// Background mode (GUI)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
};
|
||||
|
||||
|
||||
@@ -103,6 +123,8 @@ struct Command {
|
||||
std::string name; // stable, unique name (e.g., "save", "save-as")
|
||||
std::string help; // short help/description
|
||||
CommandHandler handler;
|
||||
// Public commands are exposed in the ": " prompt (C-k ;)
|
||||
bool isPublic = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -131,4 +153,4 @@ bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), i
|
||||
|
||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
#endif // KTE_COMMAND_H
|
||||
#endif // KTE_COMMAND_H
|
||||
75
Editor.cc
75
Editor.cc
@@ -3,6 +3,9 @@
|
||||
#include <filesystem>
|
||||
|
||||
#include "Editor.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/CppHighlighter.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
Editor::Editor() = default;
|
||||
@@ -151,7 +154,32 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
return cur.OpenFromFile(path, err);
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok)
|
||||
return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +187,30 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
@@ -173,6 +225,27 @@ Editor::SwitchTo(std::size_t index)
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||
if (!b.Filetype().empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
58
Editor.h
58
Editor.h
@@ -32,6 +32,16 @@ public:
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::size_t ContentRows() const
|
||||
{
|
||||
// Always compute from current rows_ to avoid stale values.
|
||||
// Reserve 1 row for status line.
|
||||
if (rows_ == 0)
|
||||
return 1;
|
||||
return std::max<std::size_t>(1, rows_ - 1);
|
||||
}
|
||||
|
||||
|
||||
// Mode and flags (mirroring legacy fields)
|
||||
void SetMode(int m)
|
||||
{
|
||||
@@ -302,7 +312,22 @@ public:
|
||||
|
||||
|
||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
|
||||
enum class PromptKind {
|
||||
None = 0,
|
||||
Search,
|
||||
RegexSearch,
|
||||
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
||||
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
||||
OpenFile,
|
||||
SaveAs,
|
||||
Confirm,
|
||||
BufferSwitch,
|
||||
GotoLine,
|
||||
Chdir,
|
||||
ReplaceFind, // step 1 of Search & Replace: find what
|
||||
ReplaceWith, // step 2 of Search & Replace: replace with
|
||||
Command // generic command prompt (": ")
|
||||
};
|
||||
|
||||
|
||||
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
||||
@@ -507,6 +532,35 @@ private:
|
||||
// GUI-only state (safe no-op in terminal builds)
|
||||
bool file_picker_visible_ = false;
|
||||
std::string file_picker_dir_;
|
||||
|
||||
// Temporary state for Search & Replace flow
|
||||
public:
|
||||
void SetReplaceFindTmp(const std::string &s)
|
||||
{
|
||||
replace_find_tmp_ = s;
|
||||
}
|
||||
|
||||
|
||||
void SetReplaceWithTmp(const std::string &s)
|
||||
{
|
||||
replace_with_tmp_ = s;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] const std::string &ReplaceFindTmp() const
|
||||
{
|
||||
return replace_find_tmp_;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] const std::string &ReplaceWithTmp() const
|
||||
{
|
||||
return replace_with_tmp_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string replace_find_tmp_;
|
||||
std::string replace_with_tmp_;
|
||||
};
|
||||
|
||||
#endif // KTE_EDITOR_H
|
||||
#endif // KTE_EDITOR_H
|
||||
29
GUIConfig.cc
29
GUIConfig.cc
@@ -23,7 +23,7 @@ default_config_path()
|
||||
{
|
||||
const char *home = std::getenv("HOME");
|
||||
if (!home || !*home)
|
||||
return std::string();
|
||||
return {};
|
||||
std::string path(home);
|
||||
path += "/.config/kte/kge.ini";
|
||||
return path;
|
||||
@@ -34,7 +34,8 @@ GUIConfig
|
||||
GUIConfig::Load()
|
||||
{
|
||||
GUIConfig cfg; // defaults already set
|
||||
std::string path = default_config_path();
|
||||
const std::string path = default_config_path();
|
||||
|
||||
if (!path.empty()) {
|
||||
cfg.LoadFromFile(path);
|
||||
}
|
||||
@@ -98,10 +99,30 @@ GUIConfig::LoadFromFile(const std::string &path)
|
||||
try {
|
||||
v = std::stof(val);
|
||||
} catch (...) {}
|
||||
if (v > 0.0f)
|
||||
if (v > 0.0f) {
|
||||
font_size = v;
|
||||
}
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "light" || v == "dark")
|
||||
background = v;
|
||||
} else if (key == "syntax") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "1" || v == "on" || v == "true" || v == "yes") {
|
||||
syntax = true;
|
||||
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
|
||||
syntax = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
18
GUIConfig.h
18
GUIConfig.h
@@ -12,10 +12,18 @@
|
||||
|
||||
class GUIConfig {
|
||||
public:
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
std::string theme = "nord";
|
||||
// Background mode for themes that support light/dark variants
|
||||
// Values: "dark" (default), "light"
|
||||
std::string background = "dark";
|
||||
|
||||
// Default syntax highlighting state for GUI (kge): on/off
|
||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||
bool syntax = true; // default: enabled
|
||||
|
||||
// Load from default path: $HOME/.config/kte/kge.ini
|
||||
static GUIConfig Load();
|
||||
@@ -24,4 +32,4 @@ public:
|
||||
bool LoadFromFile(const std::string &path);
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_CONFIG_H
|
||||
#endif // KTE_GUI_CONFIG_H
|
||||
114
GUIFrontend.cc
114
GUIFrontend.cc
@@ -15,6 +15,9 @@
|
||||
#include "GUIFrontend.h"
|
||||
#include "Font.h" // embedded default font (DefaultFontRegular)
|
||||
#include "GUIConfig.h"
|
||||
#include "GUITheme.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
#ifndef KTE_FONT_SIZE
|
||||
@@ -31,8 +34,8 @@ GUIFrontend::Init(Editor &ed)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load GUI configuration (fullscreen, columns/rows, font size)
|
||||
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load();
|
||||
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||
GUIConfig cfg = GUIConfig::Load();
|
||||
|
||||
// GL attributes for core profile
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||
@@ -46,7 +49,7 @@ GUIFrontend::Init(Editor &ed)
|
||||
// Compute desired window size from config
|
||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
|
||||
if (fullscreen) {
|
||||
if (cfg.fullscreen) {
|
||||
// "Fullscreen": fill the usable bounds of the primary display.
|
||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
||||
SDL_Rect usable{};
|
||||
@@ -60,8 +63,8 @@ GUIFrontend::Init(Editor &ed)
|
||||
#endif
|
||||
} else {
|
||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
||||
int w = static_cast<int>(columns * font_size);
|
||||
int h = static_cast<int>((rows * 2) * font_size);
|
||||
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
||||
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
||||
|
||||
// As a safety, clamp to display usable bounds if retrievable
|
||||
SDL_Rect usable{};
|
||||
@@ -74,7 +77,7 @@ GUIFrontend::Init(Editor &ed)
|
||||
}
|
||||
|
||||
window_ = SDL_CreateWindow(
|
||||
"kge - kyle's text editor " KTE_VERSION_STR,
|
||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
width_, height_,
|
||||
win_flags);
|
||||
@@ -85,7 +88,7 @@ GUIFrontend::Init(Editor &ed)
|
||||
// macOS: when "fullscreen" is requested, position the window at the
|
||||
// top-left of the usable display area to mimic fullscreen while keeping
|
||||
// the system menu bar visible.
|
||||
if (fullscreen) {
|
||||
if (cfg.fullscreen) {
|
||||
SDL_Rect usable{};
|
||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
||||
@@ -105,6 +108,45 @@ GUIFrontend::Init(Editor &ed)
|
||||
(void) io;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
return false;
|
||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||
@@ -132,7 +174,7 @@ GUIFrontend::Init(Editor &ed)
|
||||
#endif
|
||||
|
||||
// Initialize GUI font from embedded default (use configured size or compiled default)
|
||||
LoadGuiFont_(nullptr, (float) font_size);
|
||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -161,28 +203,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
input_.ProcessSDLEvent(e);
|
||||
}
|
||||
|
||||
// Execute pending mapped inputs (drain queue)
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
||||
const std::string before = ed.KillRingHead();
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
const std::string after = ed.KillRingHead();
|
||||
if (after != before && !after.empty()) {
|
||||
// Update the system clipboard to mirror the kill ring head in GUI
|
||||
SDL_SetClipboardText(after.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Start a new ImGui frame
|
||||
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
@@ -211,7 +232,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||
|
||||
// Visible content rows inside the scroll child
|
||||
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||
auto content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||
// Editor::Rows includes the status line; add 1 back for it.
|
||||
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||
@@ -222,6 +243,27 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
||||
const std::string before = ed.KillRingHead();
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
const std::string after = ed.KillRingHead();
|
||||
if (after != before && !after.empty()) {
|
||||
// Update the system clipboard to mirror the kill ring head in GUI
|
||||
SDL_SetClipboardText(after.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// No runtime font UI; always use embedded font.
|
||||
|
||||
// Draw editor UI
|
||||
@@ -261,11 +303,11 @@ GUIFrontend::Shutdown()
|
||||
bool
|
||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
io.Fonts->Clear();
|
||||
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||
(void *) DefaultFontRegularCompressedData,
|
||||
(int) DefaultFontRegularCompressedSize,
|
||||
const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||
DefaultFontBoldCompressedData,
|
||||
DefaultFontBoldCompressedSize,
|
||||
size_px);
|
||||
if (!font) {
|
||||
font = io.Fonts->AddFontDefault();
|
||||
@@ -276,4 +318,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
}
|
||||
|
||||
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
void Shutdown() override;
|
||||
|
||||
private:
|
||||
bool LoadGuiFont_(const char *path, float size_px);
|
||||
static bool LoadGuiFont_(const char *path, float size_px);
|
||||
|
||||
GUIInputHandler input_{};
|
||||
GUIRenderer renderer_{};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <ncurses.h>
|
||||
|
||||
#include <SDL.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include "GUIInputHandler.h"
|
||||
#include "KKeymap.h"
|
||||
@@ -91,12 +93,14 @@ map_key(const SDL_Keycode key,
|
||||
out = {true, CommandId::Backspace, "", 0};
|
||||
return true;
|
||||
case SDLK_TAB:
|
||||
// Insert a literal tab character
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg = "\t";
|
||||
out.count = 0;
|
||||
return true;
|
||||
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||
// If k-prefix is active, let the k-prefix handler below consume the key
|
||||
// (so Tab doesn't leave k-prefix stuck).
|
||||
if (!k_prefix) {
|
||||
out = {true, CommandId::InsertText, std::string("\t"), 0};
|
||||
return true;
|
||||
}
|
||||
break; // fall through so k-prefix handler can process
|
||||
case SDLK_RETURN:
|
||||
case SDLK_KP_ENTER:
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
@@ -281,7 +285,11 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
bool produced = false;
|
||||
switch (e.type) {
|
||||
case SDL_MOUSEWHEEL: {
|
||||
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
|
||||
// Map vertical wheel to viewport scrolling (ScrollUp/ScrollDown)
|
||||
// Note: We don't check WantCaptureMouse here because ImGui sets it to true
|
||||
// whenever the mouse is over any ImGui window (including our editor content area).
|
||||
// The NoScrollWithMouse flag on the child window prevents ImGui from handling
|
||||
// scroll internally, so we can safely process wheel events ourselves.
|
||||
int dy = e.wheel.y;
|
||||
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
||||
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
|
||||
@@ -289,7 +297,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
#endif
|
||||
if (dy != 0) {
|
||||
int repeat = dy > 0 ? dy : -dy;
|
||||
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown;
|
||||
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
@@ -348,13 +356,19 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
uarg_text_,
|
||||
mi);
|
||||
|
||||
// If we inserted a TAB on KEYDOWN, suppress any subsequent SDL_TEXTINPUT
|
||||
// for this keystroke to avoid double insertion on platforms that emit it.
|
||||
if (produced && mi.hasCommand && mi.id == CommandId::InsertText && mi.arg == "\t") {
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
|
||||
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
||||
// Digits without shift, or a plain '-'
|
||||
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
|
||||
const bool is_minus_key = (key == SDLK_MINUS);
|
||||
if (uarg_active_ && uarg_collecting_ && (is_digit_key || is_minus_key)) {
|
||||
if (uarg_active_ && uarg_collecting_ &&(is_digit_key || is_minus_key)) {
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
}
|
||||
@@ -520,11 +534,21 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
break;
|
||||
}
|
||||
if (!k_prefix_ && e.text.text[0] != '\0') {
|
||||
mi.hasCommand = true;
|
||||
mi.id = CommandId::InsertText;
|
||||
mi.arg = std::string(e.text.text);
|
||||
mi.count = 0;
|
||||
produced = true;
|
||||
// Ensure InsertText never carries a newline; those must originate from KEYDOWN
|
||||
std::string text(e.text.text);
|
||||
// Strip any CR/LF that might slip through from certain platforms/IME behaviors
|
||||
text.erase(std::remove(text.begin(), text.end(), '\n'), text.end());
|
||||
text.erase(std::remove(text.begin(), text.end(), '\r'), text.end());
|
||||
if (!text.empty()) {
|
||||
mi.hasCommand = true;
|
||||
mi.id = CommandId::InsertText;
|
||||
mi.arg = std::move(text);
|
||||
mi.count = 0;
|
||||
produced = true;
|
||||
} else {
|
||||
// Nothing to insert after filtering; consume the event
|
||||
produced = true;
|
||||
}
|
||||
} else {
|
||||
produced = true; // consumed while k-prefix is active
|
||||
}
|
||||
@@ -536,7 +560,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
|
||||
if (produced && mi.hasCommand) {
|
||||
// Attach universal-argument count if present, then clear the state
|
||||
if (uarg_active_ && mi.id != CommandId::UArgStatus) {
|
||||
if (uarg_active_ &&mi
|
||||
|
||||
.
|
||||
id != CommandId::UArgStatus
|
||||
)
|
||||
{
|
||||
int count = 0;
|
||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
||||
@@ -569,4 +598,4 @@ GUIInputHandler::Poll(MappedInput &out)
|
||||
out = q_.front();
|
||||
q_.pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
663
GUIRenderer.cc
663
GUIRenderer.cc
@@ -1,13 +1,17 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <regex>
|
||||
|
||||
#include "GUIRenderer.h"
|
||||
#include "Highlight.h"
|
||||
#include "GUITheme.h"
|
||||
#include "Buffer.h"
|
||||
#include "Command.h"
|
||||
#include "Editor.h"
|
||||
@@ -42,6 +46,8 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGui::SetNextWindowSize(main_sz);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoScrollbar
|
||||
| ImGuiWindowFlags_NoScrollWithMouse
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoCollapse
|
||||
@@ -60,55 +66,66 @@ GUIRenderer::Draw(Editor &ed)
|
||||
if (!buf) {
|
||||
ImGui::TextUnformatted("[no buffer]");
|
||||
} else {
|
||||
const auto &lines = buf->Rows();
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Detect click-to-move inside this scroll region
|
||||
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
float scroll_x = ImGui::GetScrollX();
|
||||
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
|
||||
const auto &lines = buf->Rows();
|
||||
std::size_t cy = buf->Cury();
|
||||
std::size_t cx = buf->Curx();
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float space_w = ImGui::CalcTextSize(" ").x;
|
||||
|
||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||
// This prevents clicks/wheel from being immediately overridden by stale offsets.
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
|
||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
||||
}
|
||||
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Get child window position and scroll for click handling
|
||||
ImVec2 child_window_pos = ImGui::GetWindowPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
float scroll_x = ImGui::GetScrollX();
|
||||
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
|
||||
|
||||
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||
bool forced_scroll = false;
|
||||
{
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||
|
||||
// Detect programmatic change (e.g., keyboard navigation ensured visibility)
|
||||
// Check if rowoffs was programmatically changed this frame
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
forced_scroll = true;
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
ImGui::SetScrollX(static_cast<float>(buf_coloffs) * space_w);
|
||||
scroll_x = ImGui::GetScrollX();
|
||||
forced_scroll = true;
|
||||
}
|
||||
// If user scrolled, update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
|
||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||
mbuf->Coloffs());
|
||||
}
|
||||
}
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||
@@ -116,11 +133,12 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
// Update trackers for next frame
|
||||
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
prev_buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
prev_scroll_y = ImGui::GetScrollY();
|
||||
prev_scroll_x = ImGui::GetScrollX();
|
||||
prev_scroll_y = scroll_y;
|
||||
prev_scroll_x = scroll_x;
|
||||
}
|
||||
prev_buf_rowoffs = buf_rowoffs;
|
||||
prev_buf_coloffs = buf_coloffs;
|
||||
|
||||
// Synchronize cursor and scrolling.
|
||||
// Ensure the cursor is visible even on the first frame or when it didn't move,
|
||||
// unless we already forced scrolling from Buffer::Rowoffs this frame.
|
||||
@@ -149,28 +167,26 @@ GUIRenderer::Draw(Editor &ed)
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(std::max(0L, first_row));
|
||||
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
// Compute viewport-relative row so (0) is top row of the visible area
|
||||
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
|
||||
long vy = static_cast<long>(vy_f);
|
||||
if (vy < 0)
|
||||
vy = 0;
|
||||
// Compute content-relative position accounting for scroll
|
||||
// mp.y - child_window_pos.y gives us pixels from top of child window
|
||||
// Adding scroll_y gives us pixels from top of content (buffer row 0)
|
||||
float content_y = (mp.y - child_window_pos.y) + scroll_y;
|
||||
long by_l = static_cast<long>(content_y / row_h);
|
||||
if (by_l < 0)
|
||||
by_l = 0;
|
||||
|
||||
// Clamp vy within visible content height to avoid huge jumps
|
||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||
float child_h = (cr_max.y - cr_min.y);
|
||||
long vis_rows = static_cast<long>(child_h / row_h);
|
||||
if (vis_rows < 1)
|
||||
vis_rows = 1;
|
||||
if (vy >= vis_rows)
|
||||
vy = vis_rows - 1;
|
||||
|
||||
// Translate viewport row to buffer row using Buffer::Rowoffs
|
||||
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
||||
// Convert to buffer row
|
||||
std::size_t by = static_cast<std::size_t>(by_l);
|
||||
if (by >= lines.size()) {
|
||||
if (!lines.empty())
|
||||
by = lines.size() - 1;
|
||||
@@ -178,79 +194,135 @@ GUIRenderer::Draw(Editor &ed)
|
||||
by = 0;
|
||||
}
|
||||
|
||||
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
||||
float px = (mp.x - list_origin.x - scroll_x);
|
||||
if (px < 0.0f)
|
||||
px = 0.0f;
|
||||
// Compute content-relative X position accounting for scroll
|
||||
// mp.x - child_window_pos.x gives us pixels from left edge of child window
|
||||
// Adding scroll_x gives us pixels from left edge of content (column 0)
|
||||
float content_x = (mp.x - child_window_pos.x) + scroll_x;
|
||||
if (content_x < 0.0f)
|
||||
content_x = 0.0f;
|
||||
|
||||
// Convert pixel X to a render-column target including horizontal col offset
|
||||
// Use our own tab expansion of width 8 to match command layer logic.
|
||||
const std::string &line_clicked = lines[by];
|
||||
const std::size_t tabw = 8;
|
||||
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
||||
// then translate to viewport-space by subtracting Coloffs.
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
std::size_t rx_abs = 0; // absolute rendered column
|
||||
std::size_t i = 0; // source column iterator
|
||||
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
||||
if (lines.empty()) {
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
||||
} else {
|
||||
// Convert pixel X to source column accounting for tabs
|
||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||
const std::size_t tabw = 8;
|
||||
|
||||
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
||||
if (!line_clicked.empty() && coloffs > 0) {
|
||||
while (i < line_clicked.size() && rx_abs < coloffs) {
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx_abs += (tabw - (rx_abs % tabw));
|
||||
} else {
|
||||
rx_abs += 1;
|
||||
// Iterate through source columns, computing rendered position, to find closest match
|
||||
std::size_t rx = 0; // rendered column position
|
||||
std::size_t best_col = 0;
|
||||
float best_dist = std::numeric_limits<float>::infinity();
|
||||
|
||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
||||
// Check current position
|
||||
float rx_px = static_cast<float>(rx) * space_w;
|
||||
float dist = std::fabs(content_x - rx_px);
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best_col = i;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// Now search for closest source column to clicked px within/after viewport
|
||||
std::size_t best_col = i; // default to first visible column
|
||||
float best_dist = std::numeric_limits<float>::infinity();
|
||||
while (true) {
|
||||
// For i in [current..size], evaluate candidate including the implicit end position
|
||||
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
||||
float rx_px = static_cast<float>(rx_view) * space_w;
|
||||
float dist = std::fabs(px - rx_px);
|
||||
if (dist <= best_dist) {
|
||||
best_dist = dist;
|
||||
best_col = i;
|
||||
// Advance to next position if not at end
|
||||
if (i < line_clicked.size()) {
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx += (tabw - (rx % tabw));
|
||||
} else {
|
||||
rx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i == line_clicked.size())
|
||||
break;
|
||||
// advance to next source column
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx_abs += (tabw - (rx_abs % tabw));
|
||||
} else {
|
||||
rx_abs += 1;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
// Dispatch absolute buffer coordinates (row:col)
|
||||
char tmp[64];
|
||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||
// Dispatch absolute buffer coordinates (row:col)
|
||||
char tmp[64];
|
||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||
}
|
||||
}
|
||||
// Cache current horizontal offset in rendered columns
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
const std::string &line = lines[i];
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
std::string line = static_cast<std::string>(lines[i]);
|
||||
|
||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||
const std::size_t tabw = 8;
|
||||
std::string expanded;
|
||||
expanded.reserve(line.size() + 16);
|
||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||
// Emit entire line (ImGui child scrolling will handle clipping)
|
||||
// Compute search highlight ranges for this line in source indices
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||
if (search_mode) {
|
||||
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
||||
if (ed.PromptActive() && (
|
||||
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||
try {
|
||||
std::regex rx(ed.SearchQuery());
|
||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
const auto &m = *it;
|
||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||
hl_src_ranges.emplace_back(sx, ex);
|
||||
}
|
||||
} catch (const std::regex_error &) {
|
||||
// ignore invalid patterns here; status line already shows the error
|
||||
}
|
||||
} else {
|
||||
const std::string &q = ed.SearchQuery();
|
||||
std::size_t pos = 0;
|
||||
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
||||
hl_src_ranges.emplace_back(pos, pos + q.size());
|
||||
pos += q.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
std::size_t s = 0;
|
||||
while (s < upto_src_exclusive && s < line.size()) {
|
||||
if (line[s] == '\t')
|
||||
rx += (tabw - (rx % tabw));
|
||||
else
|
||||
rx += 1;
|
||||
++s;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
// Draw background highlights (under text)
|
||||
if (search_mode && !hl_src_ranges.empty()) {
|
||||
// Current match emphasis
|
||||
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
|
||||
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
|
||||
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||
for (const auto &rg: hl_src_ranges) {
|
||||
std::size_t sx = rg.first, ex = rg.second;
|
||||
std::size_t rx_start = src_to_rx(sx);
|
||||
std::size_t rx_end = src_to_rx(ex);
|
||||
// Apply horizontal scroll offset
|
||||
if (rx_end <= coloffs_now)
|
||||
continue; // fully left of view
|
||||
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
||||
std::size_t vx1 = rx_end - coloffs_now;
|
||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
line_pos.y + line_h);
|
||||
// Choose color: current match stronger
|
||||
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||
ImU32 col = is_current
|
||||
? IM_COL32(255, 220, 120, 140)
|
||||
: IM_COL32(200, 200, 0, 90);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
// Emit spaces for the tab
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
@@ -259,7 +331,44 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size())
|
||||
continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0)
|
||||
continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
|
||||
// Draw a visible cursor indicator on the current line
|
||||
if (i == cy) {
|
||||
@@ -281,160 +390,220 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// Status bar spanning full width
|
||||
ImGui::Separator();
|
||||
// Status bar spanning full width
|
||||
ImGui::Separator();
|
||||
|
||||
// Compute full content width and draw a filled background rectangle
|
||||
ImVec2 win_pos = ImGui::GetWindowPos();
|
||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||
float x0 = win_pos.x + cr_min.x;
|
||||
float x1 = win_pos.x + cr_max.x;
|
||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||
float bar_h = ImGui::GetFrameHeight();
|
||||
ImVec2 p0(x0, cursor.y);
|
||||
ImVec2 p1(x1, cursor.y + bar_h);
|
||||
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||
// If a prompt is active, replace the entire status bar with the prompt text
|
||||
if (ed.PromptActive()) {
|
||||
std::string msg = ed.PromptLabel();
|
||||
if (!msg.empty()) msg += ": ";
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
const char *home_c = std::getenv("HOME");
|
||||
if (home_c && *home_c) {
|
||||
std::string home(home_c);
|
||||
if (ptext.rfind(home, 0) == 0) {
|
||||
std::string rest = ptext.substr(home.size());
|
||||
if (rest.empty())
|
||||
ptext = "~";
|
||||
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||
ptext = std::string("~") + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
msg += ptext;
|
||||
|
||||
float pad = 6.f;
|
||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||
float left_x = p0.x + pad;
|
||||
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(msg.c_str());
|
||||
ImGui::PopClipRect();
|
||||
// Advance cursor to after the bar to keep layout consistent
|
||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||
} else {
|
||||
// Build left text
|
||||
std::string left;
|
||||
left.reserve(256);
|
||||
left += "kge"; // GUI app name
|
||||
left += " ";
|
||||
left += KTE_VERSION_STR;
|
||||
std::string fname;
|
||||
try {
|
||||
fname = ed.DisplayNameFor(*buf);
|
||||
} catch (...) {
|
||||
fname = buf->Filename();
|
||||
try {
|
||||
fname = std::filesystem::path(fname).filename().string();
|
||||
} catch (...) {}
|
||||
}
|
||||
left += " ";
|
||||
// Insert buffer position prefix "[x/N] " before filename
|
||||
{
|
||||
std::size_t total = ed.BufferCount();
|
||||
if (total > 0) {
|
||||
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
||||
left += "[";
|
||||
left += std::to_string(static_cast<unsigned long long>(idx1));
|
||||
left += "/";
|
||||
left += std::to_string(static_cast<unsigned long long>(total));
|
||||
left += "] ";
|
||||
}
|
||||
}
|
||||
left += fname;
|
||||
if (buf->Dirty())
|
||||
left += " *";
|
||||
// Append total line count as "<n>L"
|
||||
{
|
||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||
left += " ";
|
||||
left += std::to_string(lcount);
|
||||
left += "L";
|
||||
}
|
||||
|
||||
// Build right text (cursor/mark)
|
||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||
bool have_mark = buf->MarkSet();
|
||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||
char rbuf[128];
|
||||
if (have_mark)
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||
else
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||
std::string right = rbuf;
|
||||
|
||||
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
||||
std::string msg;
|
||||
// Compute full content width and draw a filled background rectangle
|
||||
ImVec2 win_pos = ImGui::GetWindowPos();
|
||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||
float x0 = win_pos.x + cr_min.x;
|
||||
float x1 = win_pos.x + cr_max.x;
|
||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||
float bar_h = ImGui::GetFrameHeight();
|
||||
ImVec2 p0(x0, cursor.y);
|
||||
ImVec2 p1(x1, cursor.y + bar_h);
|
||||
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||
// If a prompt is active, replace the entire status bar with the prompt text
|
||||
if (ed.PromptActive()) {
|
||||
msg = ed.PromptLabel();
|
||||
if (!msg.empty())
|
||||
msg += ": ";
|
||||
msg += ed.PromptText();
|
||||
} else {
|
||||
msg = ed.Status();
|
||||
}
|
||||
std::string label = ed.PromptLabel();
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
const char *home_c = std::getenv("HOME");
|
||||
if (home_c && *home_c) {
|
||||
std::string home(home_c);
|
||||
if (ptext.rfind(home, 0) == 0) {
|
||||
std::string rest = ptext.substr(home.size());
|
||||
if (rest.empty())
|
||||
ptext = "~";
|
||||
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||
ptext = std::string("~") + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Measurements
|
||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
||||
float pad = 6.f;
|
||||
float left_x = p0.x + pad;
|
||||
float right_x = p1.x - pad - right_sz.x;
|
||||
if (right_x < left_x + left_sz.x + pad) {
|
||||
// Not enough room; clip left to fit
|
||||
float max_left = std::max(0.0f, right_x - left_x - pad);
|
||||
if (max_left < left_sz.x && max_left > 10.0f) {
|
||||
// Render a clipped left using a child region
|
||||
float pad = 6.f;
|
||||
float left_x = p0.x + pad;
|
||||
float right_x = p1.x - pad;
|
||||
float max_px = std::max(0.0f, right_x - left_x);
|
||||
|
||||
std::string prefix;
|
||||
if (kind == Editor::PromptKind::Command) {
|
||||
prefix = ": ";
|
||||
} else if (!label.empty()) {
|
||||
prefix = label + ": ";
|
||||
}
|
||||
|
||||
// Compose showing right-end of filename portion when too long for space
|
||||
std::string final_msg;
|
||||
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
||||
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
||||
// Trim from left until it fits by pixel width
|
||||
std::string tail = ptext;
|
||||
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
||||
if (tail_sz.x > avail_px) {
|
||||
// Remove leading chars until it fits
|
||||
// Use a simple loop; text lengths are small here
|
||||
size_t start = 0;
|
||||
// To avoid O(n^2) worst-case, remove chunks
|
||||
while (start < tail.size()) {
|
||||
// Estimate how many chars to skip based on ratio
|
||||
float ratio = tail_sz.x / avail_px;
|
||||
size_t skip = ratio > 1.5f
|
||||
? std::min(tail.size() - start,
|
||||
(size_t) std::max<size_t>(
|
||||
1, (size_t) (tail.size() / 4)))
|
||||
: 1;
|
||||
start += skip;
|
||||
std::string candidate = tail.substr(start);
|
||||
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
||||
if (cand_sz.x <= avail_px) {
|
||||
tail = candidate;
|
||||
tail_sz = cand_sz;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
||||
// As a last resort, ensure fit by chopping exactly
|
||||
// binary reduce
|
||||
size_t lo = 0, hi = tail.size();
|
||||
while (lo < hi) {
|
||||
size_t mid = (lo + hi) / 2;
|
||||
std::string cand = tail.substr(mid);
|
||||
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
|
||||
hi = mid;
|
||||
else
|
||||
lo = mid + 1;
|
||||
}
|
||||
tail = tail.substr(lo);
|
||||
}
|
||||
}
|
||||
final_msg = prefix + tail;
|
||||
} else {
|
||||
final_msg = prefix + ptext;
|
||||
}
|
||||
|
||||
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
||||
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(final_msg.c_str());
|
||||
ImGui::PopClipRect();
|
||||
// Advance cursor to after the bar to keep layout consistent
|
||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||
} else {
|
||||
// Build left text
|
||||
std::string left;
|
||||
left.reserve(256);
|
||||
left += "kge"; // GUI app name
|
||||
left += " ";
|
||||
left += KTE_VERSION_STR;
|
||||
std::string fname;
|
||||
try {
|
||||
fname = ed.DisplayNameFor(*buf);
|
||||
} catch (...) {
|
||||
fname = buf->Filename();
|
||||
try {
|
||||
fname = std::filesystem::path(fname).filename().string();
|
||||
} catch (...) {}
|
||||
}
|
||||
left += " ";
|
||||
// Insert buffer position prefix "[x/N] " before filename
|
||||
{
|
||||
std::size_t total = ed.BufferCount();
|
||||
if (total > 0) {
|
||||
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
||||
left += "[";
|
||||
left += std::to_string(static_cast<unsigned long long>(idx1));
|
||||
left += "/";
|
||||
left += std::to_string(static_cast<unsigned long long>(total));
|
||||
left += "] ";
|
||||
}
|
||||
}
|
||||
left += fname;
|
||||
if (buf->Dirty())
|
||||
left += " *";
|
||||
// Append total line count as "<n>L"
|
||||
{
|
||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||
left += " ";
|
||||
left += std::to_string(lcount);
|
||||
left += "L";
|
||||
}
|
||||
|
||||
// Build right text (cursor/mark)
|
||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||
bool have_mark = buf->MarkSet();
|
||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||
char rbuf[128];
|
||||
if (have_mark)
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||
else
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||
std::string right = rbuf;
|
||||
|
||||
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
||||
std::string msg;
|
||||
if (ed.PromptActive()) {
|
||||
msg = ed.PromptLabel();
|
||||
if (!msg.empty())
|
||||
msg += ": ";
|
||||
msg += ed.PromptText();
|
||||
} else {
|
||||
msg = ed.Status();
|
||||
}
|
||||
|
||||
// Measurements
|
||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
||||
float pad = 6.f;
|
||||
float left_x = p0.x + pad;
|
||||
float right_x = p1.x - pad - right_sz.x;
|
||||
if (right_x < left_x + left_sz.x + pad) {
|
||||
// Not enough room; clip left to fit
|
||||
float max_left = std::max(0.0f, right_x - left_x - pad);
|
||||
if (max_left < left_sz.x && max_left > 10.0f) {
|
||||
// Render a clipped left using a child region
|
||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
||||
ImGui::TextUnformatted(left.c_str());
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
} else {
|
||||
// Draw left normally
|
||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
||||
ImGui::TextUnformatted(left.c_str());
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
} else {
|
||||
// Draw left normally
|
||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(left.c_str());
|
||||
}
|
||||
|
||||
// Draw right
|
||||
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(right.c_str());
|
||||
// Draw right
|
||||
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
|
||||
p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(right.c_str());
|
||||
|
||||
// Draw middle message centered in remaining space
|
||||
if (!msg.empty()) {
|
||||
float mid_left = left_x + left_sz.x + pad;
|
||||
float mid_right = std::max(right_x - pad, mid_left);
|
||||
float mid_w = std::max(0.0f, mid_right - mid_left);
|
||||
if (mid_w > 1.0f) {
|
||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
||||
// Clip to middle region
|
||||
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
||||
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(msg.c_str());
|
||||
ImGui::PopClipRect();
|
||||
// Draw middle message centered in remaining space
|
||||
if (!msg.empty()) {
|
||||
float mid_left = left_x + left_sz.x + pad;
|
||||
float mid_right = std::max(right_x - pad, mid_left);
|
||||
float mid_w = std::max(0.0f, mid_right - mid_left);
|
||||
if (mid_w > 1.0f) {
|
||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
||||
// Clip to middle region
|
||||
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
||||
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||
ImGui::TextUnformatted(msg.c_str());
|
||||
ImGui::PopClipRect();
|
||||
}
|
||||
}
|
||||
// Advance cursor to after the bar to keep layout consistent
|
||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||
}
|
||||
// Advance cursor to after the bar to keep layout consistent
|
||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
@@ -588,4 +757,4 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ed.SetFilePickerVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
415
GUITheme.h
Normal file
415
GUITheme.h
Normal file
@@ -0,0 +1,415 @@
|
||||
// GUITheme.h — ImGui theming helpers and background mode
|
||||
#pragma once
|
||||
|
||||
#include <imgui.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include "themes/ThemeHelpers.h"
|
||||
|
||||
namespace kte {
|
||||
// Background mode selection for light/dark palettes
|
||||
enum class BackgroundMode { Light, Dark };
|
||||
|
||||
// Global background mode; default to Dark to match prior defaults
|
||||
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||
|
||||
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||
enum class ThemeId {
|
||||
EInk = 0,
|
||||
GruvboxDarkMedium = 1,
|
||||
GruvboxLightMedium = 1, // alias to unified gruvbox index
|
||||
Nord = 2,
|
||||
Plan9 = 3,
|
||||
Solarized = 4,
|
||||
};
|
||||
|
||||
// Current theme tracking
|
||||
static inline auto gCurrentTheme = ThemeId::Nord;
|
||||
static inline std::size_t gCurrentThemeIndex = 0;
|
||||
|
||||
// Forward declarations for helpers used below
|
||||
static size_t ThemeIndexFromId(ThemeId id);
|
||||
|
||||
static ThemeId ThemeIdFromIndex(size_t idx);
|
||||
|
||||
// Helpers to set/query background mode
|
||||
static void
|
||||
SetBackgroundMode(const BackgroundMode m)
|
||||
{
|
||||
gBackgroundMode = m;
|
||||
}
|
||||
|
||||
|
||||
static BackgroundMode
|
||||
GetBackgroundMode()
|
||||
{
|
||||
return gBackgroundMode;
|
||||
}
|
||||
|
||||
|
||||
static inline const char *
|
||||
BackgroundModeName()
|
||||
{
|
||||
return gBackgroundMode == BackgroundMode::Light ? "light" : "dark";
|
||||
}
|
||||
|
||||
|
||||
// Include individual theme implementations split under ./themes
|
||||
#include "themes/Nord.h"
|
||||
#include "themes/Plan9.h"
|
||||
#include "themes/Solarized.h"
|
||||
#include "themes/Gruvbox.h"
|
||||
#include "themes/EInk.h"
|
||||
|
||||
|
||||
// Theme abstraction and registry (generalized theme system)
|
||||
class Theme {
|
||||
public:
|
||||
virtual ~Theme() = default;
|
||||
|
||||
[[nodiscard]] virtual const char *Name() const = 0; // canonical name (e.g., "nord", "gruvbox-dark")
|
||||
virtual void Apply() const = 0; // apply to current ImGui style
|
||||
virtual ThemeId Id() = 0; // theme identifier
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
struct NordTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "nord";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
ApplyNordImGuiTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Nord;
|
||||
}
|
||||
};
|
||||
|
||||
struct GruvboxTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "gruvbox";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Light)
|
||||
ApplyGruvboxLightMediumTheme();
|
||||
else
|
||||
ApplyGruvboxDarkMediumTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
// Legacy maps to dark; unified under base id GruvboxDarkMedium
|
||||
return ThemeId::GruvboxDarkMedium;
|
||||
}
|
||||
};
|
||||
|
||||
struct EInkTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "eink";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Dark)
|
||||
ApplyEInkDarkImGuiTheme();
|
||||
else
|
||||
ApplyEInkImGuiTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::EInk;
|
||||
}
|
||||
};
|
||||
|
||||
struct SolarizedTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "solarized";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Light)
|
||||
ApplySolarizedLightTheme();
|
||||
else
|
||||
ApplySolarizedDarkTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Solarized;
|
||||
}
|
||||
};
|
||||
|
||||
struct Plan9Theme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "plan9";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
ApplyPlan9Theme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Plan9;
|
||||
}
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
static const std::vector<std::unique_ptr<Theme> > &
|
||||
ThemeRegistry()
|
||||
{
|
||||
static std::vector<std::unique_ptr<Theme> > reg;
|
||||
if (reg.empty()) {
|
||||
// Alphabetical by canonical name: eink, gruvbox, nord, plan9, solarized
|
||||
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
|
||||
// Canonical theme name for a given ThemeId (via registry order)
|
||||
[[maybe_unused]] static const char *
|
||||
ThemeName(const ThemeId id)
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
const size_t idx = ThemeIndexFromId(id);
|
||||
if (idx < reg.size())
|
||||
return reg[idx]->Name();
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
|
||||
// Helper to apply a theme by id and update current theme
|
||||
static void
|
||||
ApplyTheme(const ThemeId id)
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
const size_t idx = ThemeIndexFromId(id);
|
||||
if (idx < reg.size()) {
|
||||
reg[idx]->Apply();
|
||||
gCurrentTheme = id;
|
||||
gCurrentThemeIndex = idx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[[maybe_unused]] static ThemeId
|
||||
CurrentTheme()
|
||||
{
|
||||
return gCurrentTheme;
|
||||
}
|
||||
|
||||
|
||||
// Cycle helpers
|
||||
[[maybe_unused]] static ThemeId
|
||||
NextTheme()
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
if (reg.empty()) {
|
||||
return gCurrentTheme;
|
||||
}
|
||||
|
||||
const size_t nxt = (gCurrentThemeIndex + 1) % reg.size();
|
||||
ApplyTheme(ThemeIdFromIndex(nxt));
|
||||
return gCurrentTheme;
|
||||
}
|
||||
|
||||
|
||||
[[maybe_unused]] static ThemeId
|
||||
PrevTheme()
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
if (reg.empty()) {
|
||||
return gCurrentTheme;
|
||||
}
|
||||
|
||||
const size_t prv = (gCurrentThemeIndex + reg.size() - 1) % reg.size();
|
||||
ApplyTheme(ThemeIdFromIndex(prv));
|
||||
return gCurrentTheme;
|
||||
}
|
||||
|
||||
|
||||
// Name-based API
|
||||
[[maybe_unused]] static const Theme *
|
||||
GetThemeByName(const std::string &name)
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
for (const auto &t: reg) {
|
||||
if (name == t->Name())
|
||||
return t.get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
[[maybe_unused]] static bool
|
||||
ApplyThemeByName(const std::string &name)
|
||||
{
|
||||
// Handle aliases and background-specific names
|
||||
std::string n = name;
|
||||
// lowercase copy
|
||||
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
|
||||
if (n == "gruvbox-dark") {
|
||||
SetBackgroundMode(BackgroundMode::Dark);
|
||||
n = "gruvbox";
|
||||
} else if (n == "gruvbox-light") {
|
||||
SetBackgroundMode(BackgroundMode::Light);
|
||||
n = "gruvbox";
|
||||
} else if (n == "solarized-dark") {
|
||||
SetBackgroundMode(BackgroundMode::Dark);
|
||||
n = "solarized";
|
||||
} else if (n == "solarized-light") {
|
||||
SetBackgroundMode(BackgroundMode::Light);
|
||||
n = "solarized";
|
||||
} else if (n == "eink-dark") {
|
||||
SetBackgroundMode(BackgroundMode::Dark);
|
||||
n = "eink";
|
||||
} else if (n == "eink-light") {
|
||||
SetBackgroundMode(BackgroundMode::Light);
|
||||
n = "eink";
|
||||
}
|
||||
|
||||
const auto ® = ThemeRegistry();
|
||||
for (size_t i = 0; i < reg.size(); ++i) {
|
||||
if (n == reg[i]->Name()) {
|
||||
reg[i]->Apply();
|
||||
gCurrentThemeIndex = i;
|
||||
gCurrentTheme = ThemeIdFromIndex(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
[[maybe_unused]] static const char *
|
||||
CurrentThemeName()
|
||||
{
|
||||
const auto ® = ThemeRegistry();
|
||||
if (gCurrentThemeIndex < reg.size()) {
|
||||
return reg[gCurrentThemeIndex]->Name();
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
|
||||
// Helpers to map between legacy ThemeId and registry index
|
||||
static size_t
|
||||
ThemeIndexFromId(const ThemeId id)
|
||||
{
|
||||
switch (id) {
|
||||
case ThemeId::EInk:
|
||||
return 0;
|
||||
case ThemeId::GruvboxDarkMedium:
|
||||
return 1;
|
||||
case ThemeId::Nord:
|
||||
return 2;
|
||||
case ThemeId::Plan9:
|
||||
return 3;
|
||||
case ThemeId::Solarized:
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static ThemeId
|
||||
ThemeIdFromIndex(const size_t idx)
|
||||
{
|
||||
switch (idx) {
|
||||
default:
|
||||
case 0:
|
||||
return ThemeId::EInk;
|
||||
case 1:
|
||||
return ThemeId::GruvboxDarkMedium; // unified gruvbox
|
||||
case 2:
|
||||
return ThemeId::Nord;
|
||||
case 3:
|
||||
return ThemeId::Plan9;
|
||||
case 4:
|
||||
return ThemeId::Solarized;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||
[[maybe_unused]] static ImVec4
|
||||
SyntaxInk(const TokenKind k)
|
||||
{
|
||||
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||
// Base text
|
||||
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||
case TokenKind::Type:
|
||||
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||
case TokenKind::String:
|
||||
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Char:
|
||||
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Comment:
|
||||
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||
case TokenKind::Number:
|
||||
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||
case TokenKind::Preproc:
|
||||
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||
case TokenKind::Constant:
|
||||
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
|
||||
case TokenKind::Function:
|
||||
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
|
||||
case TokenKind::Operator:
|
||||
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||
case TokenKind::Punctuation:
|
||||
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||
case TokenKind::Identifier:
|
||||
return def;
|
||||
case TokenKind::Whitespace:
|
||||
return def;
|
||||
case TokenKind::Error:
|
||||
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
|
||||
case TokenKind::Default: default:
|
||||
return def;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
81
HelpText.cc
Normal file
81
HelpText.cc
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* HelpText.cc - embedded/customizable help content
|
||||
*/
|
||||
|
||||
#include "HelpText.h"
|
||||
|
||||
|
||||
std::string
|
||||
HelpText::Text()
|
||||
{
|
||||
// Customize the help text here. This string will be used by C-k h first.
|
||||
// You can keep it empty to fall back to the manpage or built-in defaults.
|
||||
// Note: keep newline characters as-is; the renderer splits lines on '\n'.
|
||||
|
||||
return std::string(
|
||||
"KTE - Kyle's Text Editor\n\n"
|
||||
"About:\n"
|
||||
" kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
|
||||
" WordStar/VDE-style command model with some emacs influences.\n"
|
||||
"\n"
|
||||
"K-commands (prefix C-k):\n"
|
||||
" C-k ' Toggle read-only\n"
|
||||
" C-k - Unindent region (mark required)\n"
|
||||
" C-k = Indent region (mark required)\n"
|
||||
" C-k ; Command prompt (:\\ )\n"
|
||||
" C-k C-d Kill entire line\n"
|
||||
" C-k C-q Quit now (no confirm)\n"
|
||||
" C-k C-x Save and quit\n"
|
||||
" C-k a Mark start of file, jump to end\n"
|
||||
" C-k b Switch buffer\n"
|
||||
" C-k c Close current buffer\n"
|
||||
" C-k d Kill to end of line\n"
|
||||
" C-k e Open file (prompt)\n"
|
||||
" C-k f Flush kill ring\n"
|
||||
" C-k g Jump to line\n"
|
||||
" C-k h Show this help\n"
|
||||
" C-k j Jump to mark\n"
|
||||
" C-k l Reload buffer from disk\n"
|
||||
" C-k n Previous buffer\n"
|
||||
" C-k o Change working directory (prompt)\n"
|
||||
" C-k p Next buffer\n"
|
||||
" C-k q Quit (confirm if dirty)\n"
|
||||
" C-k r Redo\n"
|
||||
" C-k s Save buffer\n"
|
||||
" C-k u Undo\n"
|
||||
" C-k v Toggle visual file picker (GUI)\n"
|
||||
" C-k w Show working directory\n"
|
||||
" C-k x Save and quit\n"
|
||||
" C-k y Yank\n"
|
||||
"\n"
|
||||
"ESC/Alt commands:\n"
|
||||
" ESC < Go to beginning of file\n"
|
||||
" ESC > Go to end of file\n"
|
||||
" ESC m Toggle mark\n"
|
||||
" ESC w Copy region to kill ring (Alt-w)\n"
|
||||
" ESC b Previous word\n"
|
||||
" ESC f Next word\n"
|
||||
" ESC d Delete next word (Alt-d)\n"
|
||||
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||
" ESC q Reflow paragraph\n"
|
||||
"\n"
|
||||
"Control keys:\n"
|
||||
" C-a C-e Line start / end\n"
|
||||
" C-b C-f Move left / right\n"
|
||||
" C-n C-p Move down / up\n"
|
||||
" C-d Delete char\n"
|
||||
" C-w / C-y Kill region / Yank\n"
|
||||
" C-s Incremental find\n"
|
||||
" C-r Regex search\n"
|
||||
" C-t Regex search & replace\n"
|
||||
" C-h Search & replace\n"
|
||||
" C-l / C-g Refresh / Cancel\n"
|
||||
" C-u [digits] Universal argument (repeat count)\n"
|
||||
"\n"
|
||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||
"\n"
|
||||
"GUI appearance (command prompt):\n"
|
||||
" : theme NAME Set GUI theme (eink, gruvbox, nord, plan9, solarized)\n"
|
||||
" : background MODE Set background: light | dark (affects eink, gruvbox, solarized)\n"
|
||||
);
|
||||
}
|
||||
17
HelpText.h
Normal file
17
HelpText.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* HelpText.h - embedded/customizable help content
|
||||
*/
|
||||
#ifndef KTE_HELPTEXT_H
|
||||
#define KTE_HELPTEXT_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class HelpText {
|
||||
public:
|
||||
// Returns the embedded help text as a single string with newlines.
|
||||
// Project maintainers can customize the returned string below
|
||||
// (in HelpText.cc) without touching the help command logic.
|
||||
static std::string Text();
|
||||
};
|
||||
|
||||
#endif // KTE_HELPTEXT_H
|
||||
37
Highlight.h
Normal file
37
Highlight.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Highlight.h - core syntax highlighting types for kte
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace kte {
|
||||
// Token kinds shared between renderers and highlighters
|
||||
enum class TokenKind {
|
||||
Default,
|
||||
Keyword,
|
||||
Type,
|
||||
String,
|
||||
Char,
|
||||
Comment,
|
||||
Number,
|
||||
Preproc,
|
||||
Constant,
|
||||
Function,
|
||||
Operator,
|
||||
Punctuation,
|
||||
Identifier,
|
||||
Whitespace,
|
||||
Error
|
||||
};
|
||||
|
||||
struct HighlightSpan {
|
||||
int col_start{0}; // inclusive, 0-based columns in buffer indices
|
||||
int col_end{0}; // exclusive
|
||||
TokenKind kind{TokenKind::Default};
|
||||
};
|
||||
|
||||
struct LineHighlight {
|
||||
std::vector<HighlightSpan> spans;
|
||||
std::uint64_t version{0}; // buffer version used for this line
|
||||
};
|
||||
} // namespace kte
|
||||
19
KKeymap.cc
19
KKeymap.cc
@@ -33,6 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
out = CommandId::Redo; // C-k r (redo)
|
||||
return true;
|
||||
}
|
||||
if (ascii_key == '\'') {
|
||||
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (k_lower) {
|
||||
case 'a':
|
||||
@@ -59,6 +63,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case 'g':
|
||||
out = CommandId::JumpToLine;
|
||||
return true;
|
||||
case 'h':
|
||||
out = CommandId::ShowHelp;
|
||||
return true;
|
||||
case 'j':
|
||||
out = CommandId::JumpToMark;
|
||||
return true;
|
||||
@@ -101,6 +108,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case '=':
|
||||
out = CommandId::IndentRegion;
|
||||
return true;
|
||||
case ';':
|
||||
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -145,6 +155,15 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
||||
case 's':
|
||||
out = CommandId::FindStart;
|
||||
return true;
|
||||
case 'r':
|
||||
out = CommandId::RegexFindStart; // C-r regex search
|
||||
return true;
|
||||
case 't':
|
||||
out = CommandId::RegexpReplace; // C-t regex search & replace
|
||||
return true;
|
||||
case 'h':
|
||||
out = CommandId::SearchReplace; // C-h: search & replace
|
||||
return true;
|
||||
case 'l':
|
||||
out = CommandId::Refresh;
|
||||
return true;
|
||||
|
||||
16
ROADMAP.md
16
ROADMAP.md
@@ -1,10 +1,12 @@
|
||||
ROADMAP / TODO:
|
||||
|
||||
- [ ] Search + Replace
|
||||
- [ ] Regex search + replace
|
||||
- [x] Search + Replace
|
||||
- [x] Regex search + replace
|
||||
- [x] Able to mark buffers as read-only
|
||||
- [x] Built-in help text
|
||||
- [x] Shorten paths in the homedir with ~
|
||||
- [x] When the filename is longer than the message window, scoot left to
|
||||
keep it in view
|
||||
- [x] Syntax highlighting
|
||||
- [ ] The undo system should actually work
|
||||
- [ ] Able to mark buffers as read-only
|
||||
- [ ] Built-in help text
|
||||
- [ ] Shorten paths in the homedir with ~
|
||||
- [ ] When the filename is longer than the message window, scoot left to
|
||||
to keep it in view
|
||||
- [ ] LSP integration
|
||||
|
||||
@@ -35,16 +35,16 @@ map_key_to_command(const int ch,
|
||||
case KEY_MOUSE: {
|
||||
MEVENT ev{};
|
||||
if (getmouse(&ev) == OK) {
|
||||
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
||||
// Mouse wheel → scroll viewport without moving cursor
|
||||
#ifdef BUTTON4_PRESSED
|
||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
out = {true, CommandId::ScrollUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef BUTTON5_PRESSED
|
||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
out = {true, CommandId::ScrollDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -320,4 +320,4 @@ TerminalInputHandler::Poll(MappedInput &out)
|
||||
{
|
||||
out = {};
|
||||
return decode_(out) && out.hasCommand;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include <ncurses.h>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
#include "TerminalRenderer.h"
|
||||
#include "Buffer.h"
|
||||
#include "Editor.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
// Version string expected to be provided by build system as KTE_VERSION_STR
|
||||
#ifndef KTE_VERSION_STR
|
||||
@@ -32,6 +34,8 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
|
||||
const Buffer *buf = ed.CurrentBuffer();
|
||||
int content_rows = rows - 1; // last line is status
|
||||
if (content_rows < 1)
|
||||
content_rows = 1;
|
||||
|
||||
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
|
||||
if (buf) {
|
||||
@@ -40,20 +44,113 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
|
||||
const int tabw = 8;
|
||||
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(rowoffs);
|
||||
int rc = std::max(0, content_rows);
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
|
||||
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
|
||||
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
|
||||
bool hl_on = false;
|
||||
int written = 0;
|
||||
std::size_t src_i = 0;
|
||||
// Compute matches for this line if search highlighting is active
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||
if (search_mode && li < lines.size()) {
|
||||
std::string sline = static_cast<std::string>(lines[li]);
|
||||
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
||||
if (ed.PromptActive() && (
|
||||
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||
try {
|
||||
std::regex rx(ed.SearchQuery());
|
||||
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
const auto &m = *it;
|
||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||
ranges.emplace_back(sx, ex);
|
||||
}
|
||||
} catch (const std::regex_error &) {
|
||||
// ignore invalid patterns here; status shows error
|
||||
}
|
||||
} else {
|
||||
const std::string &q = ed.SearchQuery();
|
||||
std::size_t pos = 0;
|
||||
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
||||
ranges.emplace_back(pos, pos + q.size());
|
||||
pos += q.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
||||
if (ranges.empty())
|
||||
return false;
|
||||
// ranges are non-overlapping and ordered by construction
|
||||
// linear scan is fine for now
|
||||
for (const auto &rg: ranges) {
|
||||
if (si < rg.first)
|
||||
break;
|
||||
if (si >= rg.first && si < rg.second)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Track current-match to optionally emphasize
|
||||
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
||||
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
||||
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
||||
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||
bool hl_on = false;
|
||||
bool cur_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
const std::string &line = lines[li];
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||
HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr)
|
||||
return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
|
||||
src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
@@ -76,16 +173,36 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
// Now render visible spaces
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen;
|
||||
// highlight by source index
|
||||
if (in_hl && !hl_on) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
@@ -108,16 +225,28 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
ch = ' ';
|
||||
from_src = false;
|
||||
}
|
||||
if (do_hl) {
|
||||
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
@@ -132,6 +261,11 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
|
||||
@@ -150,50 +284,74 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
mvaddstr(0, 0, "[no buffer]");
|
||||
}
|
||||
|
||||
// Status line (inverse)
|
||||
move(rows - 1, 0);
|
||||
attron(A_REVERSE);
|
||||
// Status line (inverse)
|
||||
move(rows - 1, 0);
|
||||
attron(A_REVERSE);
|
||||
|
||||
// Fill the status line with spaces first
|
||||
for (int i = 0; i < cols; ++i)
|
||||
addch(' ');
|
||||
// Fill the status line with spaces first
|
||||
for (int i = 0; i < cols; ++i)
|
||||
addch(' ');
|
||||
|
||||
// If a prompt is active, replace the status bar with the full prompt text
|
||||
if (ed.PromptActive()) {
|
||||
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
||||
std::string msg = ed.PromptLabel();
|
||||
if (!msg.empty())
|
||||
msg += ": ";
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
const char *home_c = std::getenv("HOME");
|
||||
if (home_c && *home_c) {
|
||||
std::string home(home_c);
|
||||
// Ensure we match only at the start
|
||||
if (ptext.rfind(home, 0) == 0) {
|
||||
std::string rest = ptext.substr(home.size());
|
||||
if (rest.empty())
|
||||
ptext = "~";
|
||||
else if (rest[0] == '/' || rest[0] == '\\')
|
||||
ptext = std::string("~") + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
msg += ptext;
|
||||
// If a prompt is active, replace the status bar with the full prompt text
|
||||
if (ed.PromptActive()) {
|
||||
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
||||
std::string label = ed.PromptLabel();
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
const char *home_c = std::getenv("HOME");
|
||||
if (home_c && *home_c) {
|
||||
std::string home(home_c);
|
||||
// Ensure we match only at the start
|
||||
if (ptext.rfind(home, 0) == 0) {
|
||||
std::string rest = ptext.substr(home.size());
|
||||
if (rest.empty())
|
||||
ptext = "~";
|
||||
else if (rest[0] == '/' || rest[0] == '\\')
|
||||
ptext = std::string("~") + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prefer keeping the tail of the filename visible when it exceeds the window
|
||||
std::string msg;
|
||||
if (kind == Editor::PromptKind::Command) {
|
||||
msg = ": ";
|
||||
} else if (!label.empty()) {
|
||||
msg = label + ": ";
|
||||
}
|
||||
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||
Editor::PromptKind::Chdir) && cols > 0) {
|
||||
int avail = cols - static_cast<int>(msg.size());
|
||||
if (avail <= 0) {
|
||||
// No room for label; fall back to showing the rightmost portion of the whole string
|
||||
std::string whole = msg + ptext;
|
||||
if ((int) whole.size() > cols)
|
||||
whole = whole.substr(whole.size() - cols);
|
||||
msg = whole;
|
||||
} else {
|
||||
if ((int) ptext.size() > avail) {
|
||||
ptext = ptext.substr(ptext.size() - avail);
|
||||
}
|
||||
msg += ptext;
|
||||
}
|
||||
} else {
|
||||
// Non-file prompts: simple concatenation and clip by terminal
|
||||
msg += ptext;
|
||||
}
|
||||
|
||||
// Draw left-aligned, clipped to width
|
||||
if (!msg.empty())
|
||||
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
||||
// Draw left-aligned, clipped to width
|
||||
if (!msg.empty())
|
||||
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
||||
|
||||
// End status rendering for prompt mode
|
||||
attroff(A_REVERSE);
|
||||
// Restore logical cursor position in content area
|
||||
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
||||
move(saved_cur_y, saved_cur_x);
|
||||
return;
|
||||
}
|
||||
// End status rendering for prompt mode
|
||||
attroff(A_REVERSE);
|
||||
// Restore logical cursor position in content area
|
||||
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
||||
move(saved_cur_y, saved_cur_x);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build left segment
|
||||
std::string left;
|
||||
@@ -233,6 +391,9 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
left += fname;
|
||||
if (b && b->Dirty())
|
||||
left += " *";
|
||||
// Append read-only indicator
|
||||
if (b && b->IsReadOnly())
|
||||
left += " [RO]";
|
||||
// Append total line count as "<n>L"
|
||||
if (b) {
|
||||
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
|
||||
@@ -281,10 +442,10 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
if (llen > 0)
|
||||
mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
||||
|
||||
// Draw right, flush to end
|
||||
int rstart = std::max(0, cols - rlen);
|
||||
if (rlen > 0)
|
||||
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
||||
// Draw right, flush to end
|
||||
int rstart = std::max(0, cols - rlen);
|
||||
if (rlen > 0)
|
||||
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
||||
|
||||
// Middle message
|
||||
const std::string &msg = ed.Status();
|
||||
@@ -300,7 +461,7 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
}
|
||||
|
||||
attroff(A_REVERSE);
|
||||
attroff(A_REVERSE);
|
||||
|
||||
// Restore terminal cursor to the content position so a visible caret
|
||||
// remains in the editing area (not on the status line).
|
||||
@@ -309,4 +470,4 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
113
UndoSystem.cc
113
UndoSystem.cc
@@ -1,5 +1,7 @@
|
||||
#include "UndoSystem.h"
|
||||
#include "Buffer.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
|
||||
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||
@@ -9,21 +11,24 @@ UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||
void
|
||||
UndoSystem::Begin(UndoType type)
|
||||
{
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Begin");
|
||||
#endif
|
||||
// Reuse pending if batching conditions are met
|
||||
const int row = static_cast<int>(buf_->Cury());
|
||||
const int col = static_cast<int>(buf_->Curx());
|
||||
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
||||
if (type == UndoType::Delete) {
|
||||
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
||||
// Forward delete: cursor stays at anchor col; expected == col
|
||||
// Forward delete: cursor stays at anchor col; keep batching when col == anchor
|
||||
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
||||
if (anchor + tree_.pending->text.size() == static_cast<std::size_t>(col)) {
|
||||
if (anchor == static_cast<std::size_t>(col)) {
|
||||
pending_prepend_ = false;
|
||||
return; // keep batching forward delete
|
||||
}
|
||||
// Backspace: cursor moved left by 1; allow extend if col + text.size() == anchor
|
||||
if (static_cast<std::size_t>(col) + tree_.pending->text.size() == anchor) {
|
||||
// Move anchor one left to new cursor column; next Append should prepend
|
||||
// Backspace: cursor moved left by exactly one position relative to current anchor.
|
||||
// Extend batch by shifting anchor left and prepending the deleted byte.
|
||||
if (static_cast<std::size_t>(col) + 1 == anchor) {
|
||||
tree_.pending->col = col;
|
||||
pending_prepend_ = true;
|
||||
return;
|
||||
@@ -47,6 +52,16 @@ UndoSystem::Begin(UndoType type)
|
||||
node->next = nullptr;
|
||||
tree_.pending = node;
|
||||
pending_prepend_ = false;
|
||||
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Begin:new");
|
||||
#endif
|
||||
// Assert pending is detached from the tree
|
||||
assert(tree_.pending && "pending must exist after Begin");
|
||||
assert(tree_.pending != tree_.root);
|
||||
assert(tree_.pending != tree_.current);
|
||||
assert(tree_.pending != tree_.saved);
|
||||
assert(!is_descendant(tree_.root, tree_.pending));
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +76,9 @@ UndoSystem::Append(char ch)
|
||||
} else {
|
||||
tree_.pending->text.push_back(ch);
|
||||
}
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Append:ch");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +88,18 @@ UndoSystem::Append(std::string_view text)
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
tree_.pending->text.append(text.data(), text.size());
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Append:sv");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::commit()
|
||||
{
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("commit:enter");
|
||||
#endif
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
|
||||
@@ -105,6 +129,11 @@ UndoSystem::commit()
|
||||
}
|
||||
tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("commit:done");
|
||||
#endif
|
||||
// post-conditions
|
||||
assert(tree_.pending == nullptr && "pending must be cleared after commit");
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +150,9 @@ UndoSystem::undo()
|
||||
apply(node, -1);
|
||||
tree_.current = parent;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("undo");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +175,9 @@ UndoSystem::redo()
|
||||
apply(next, +1);
|
||||
tree_.current = next;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("redo");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +186,9 @@ UndoSystem::mark_saved()
|
||||
{
|
||||
tree_.saved = tree_.current;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("mark_saved");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +199,9 @@ UndoSystem::discard_pending()
|
||||
delete tree_.pending;
|
||||
tree_.pending = nullptr;
|
||||
}
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("discard_pending");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +216,9 @@ UndoSystem::clear()
|
||||
}
|
||||
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("clear");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -293,3 +337,62 @@ UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
||||
{
|
||||
buf_ = &new_buf;
|
||||
}
|
||||
|
||||
|
||||
// ---- Debug helpers ----
|
||||
const char *
|
||||
UndoSystem::type_str(UndoType t)
|
||||
{
|
||||
switch (t) {
|
||||
case UndoType::Insert:
|
||||
return "Insert";
|
||||
case UndoType::Delete:
|
||||
return "Delete";
|
||||
case UndoType::Paste:
|
||||
return "Paste";
|
||||
case UndoType::Newline:
|
||||
return "Newline";
|
||||
case UndoType::DeleteRow:
|
||||
return "DeleteRow";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
||||
{
|
||||
if (!root || !target)
|
||||
return false;
|
||||
if (root == target)
|
||||
return true;
|
||||
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
||||
if (is_descendant(child, target))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::debug_log(const char *op) const
|
||||
{
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
int row = static_cast<int>(buf_->Cury());
|
||||
int col = static_cast<int>(buf_->Curx());
|
||||
const UndoNode *p = tree_.pending;
|
||||
std::fprintf(stderr,
|
||||
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
|
||||
op,
|
||||
row, col,
|
||||
(const void *) p,
|
||||
p ? type_str(p->type) : "-",
|
||||
p ? p->row : -1,
|
||||
p ? p->col : -1,
|
||||
p ? p->text.size() : 0,
|
||||
(void *) tree_.current,
|
||||
(void *) tree_.saved);
|
||||
#else
|
||||
(void) op;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define KTE_UNDOSYSTEM_H
|
||||
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "UndoTree.h"
|
||||
|
||||
@@ -39,6 +41,13 @@ private:
|
||||
void free_branch(UndoNode *node); // frees redo siblings only
|
||||
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
||||
|
||||
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
||||
void debug_log(const char *op) const;
|
||||
|
||||
static const char *type_str(UndoType t);
|
||||
|
||||
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
||||
|
||||
void update_dirty_flag();
|
||||
|
||||
Buffer *buf_;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
cmake,
|
||||
ncurses,
|
||||
SDL2,
|
||||
libGL,
|
||||
xorg,
|
||||
installShellFiles,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "kte";
|
||||
inherit version;
|
||||
|
||||
src = lib.cleanSource ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
ncurses
|
||||
SDL2
|
||||
libGL
|
||||
xorg.libX11
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DBUILD_GUI=ON"
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/bin
|
||||
cp kte $out/bin/
|
||||
cp kge $out/bin/
|
||||
|
||||
installManPage ../docs/kte.1
|
||||
installManPage ../docs/kge.1
|
||||
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
cmake,
|
||||
ncurses,
|
||||
installShellFiles,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "kte";
|
||||
inherit version;
|
||||
|
||||
src = lib.cleanSource ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
ncurses
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DBUILD_GUI=OFF"
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/bin
|
||||
cp kte $out/bin/
|
||||
|
||||
installManPage ../docs/kte.1
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
22
default.nix
22
default.nix
@@ -7,12 +7,16 @@
|
||||
libGL,
|
||||
xorg,
|
||||
installShellFiles,
|
||||
|
||||
graphical ? false,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
||||
versionLine = lib.findFirst (
|
||||
l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null
|
||||
) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
@@ -24,14 +28,16 @@ stdenv.mkDerivation {
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
ncurses
|
||||
installShellFiles
|
||||
]
|
||||
++ lib.optionals graphical [
|
||||
SDL2
|
||||
libGL
|
||||
xorg.libX11
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DBUILD_GUI=ON"
|
||||
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
];
|
||||
|
||||
@@ -40,11 +46,17 @@ stdenv.mkDerivation {
|
||||
|
||||
mkdir -p $out/bin
|
||||
cp kte $out/bin/
|
||||
cp kge $out/bin/
|
||||
|
||||
installManPage ../docs/kte.1
|
||||
installManPage ../docs/kge.1
|
||||
|
||||
''
|
||||
+ lib.optionalString graphical ''
|
||||
cp kge $out/bin/
|
||||
installManPage ../docs/kge.1
|
||||
mkdir -p $out/share/icons
|
||||
cp ../kge.png $out/share/icons/
|
||||
''
|
||||
+ ''
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
|
||||
74
docs/kge.1
74
docs/kge.1
@@ -1,7 +1,7 @@
|
||||
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
|
||||
.\"
|
||||
.\" Project homepage: https://github.com/wntrmute/kte
|
||||
.TH KGE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
||||
.TH KGE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||
.SH NAME
|
||||
kge \- Kyle's Graphical Editor (GUI-first)
|
||||
.SH SYNOPSIS
|
||||
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
|
||||
.PP
|
||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||
.TP
|
||||
.B C-k BACKSPACE
|
||||
Delete from the cursor to the beginning of the line.
|
||||
.TP
|
||||
.B C-k SPACE
|
||||
Toggle the mark.
|
||||
.B C-k '
|
||||
Toggle read-only for the current buffer.
|
||||
.TP
|
||||
.B C-k -
|
||||
If the mark is set, unindent the region.
|
||||
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
|
||||
.B C-k =
|
||||
If the mark is set, indent the region.
|
||||
.TP
|
||||
.B C-k ;
|
||||
Open the generic command prompt (": ").
|
||||
.TP
|
||||
.B C-k a
|
||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||
.TP
|
||||
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
|
||||
Delete the entire line.
|
||||
.TP
|
||||
.B C-k e
|
||||
Edit a new file.
|
||||
Edit (open) a new file.
|
||||
.TP
|
||||
.B C-k f
|
||||
Flush the kill ring.
|
||||
@@ -88,14 +88,20 @@ Flush the kill ring.
|
||||
.B C-k g
|
||||
Go to a specific line.
|
||||
.TP
|
||||
.B C-k h
|
||||
Show the built-in help (+HELP+ buffer).
|
||||
.TP
|
||||
.B C-k j
|
||||
Jump to the mark.
|
||||
.TP
|
||||
.B C-k l
|
||||
Reload the current buffer from disk.
|
||||
.TP
|
||||
.B C-k m
|
||||
Run make(1), reporting success or failure.
|
||||
.B C-k n
|
||||
Switch to the previous buffer.
|
||||
.TP
|
||||
.B C-k o
|
||||
Change working directory (prompt).
|
||||
.TP
|
||||
.B C-k p
|
||||
Switch to the next buffer.
|
||||
@@ -106,14 +112,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
||||
.B C-k C-q
|
||||
Immediately exit the editor.
|
||||
.TP
|
||||
.B C-k r
|
||||
Redo changes.
|
||||
.TP
|
||||
.B C-k s
|
||||
Save the file, prompting for a filename if needed.
|
||||
.TP
|
||||
.B C-k u
|
||||
Undo.
|
||||
.TP
|
||||
.B C-k r
|
||||
Redo changes.
|
||||
.B C-k v
|
||||
Toggle visual file picker (GUI).
|
||||
.TP
|
||||
.B C-k w
|
||||
Show the current working directory.
|
||||
.TP
|
||||
.B C-k x
|
||||
Save the file and exit. Also C-k C-x.
|
||||
@@ -121,23 +133,50 @@ Save the file and exit. Also C-k C-x.
|
||||
.B C-k y
|
||||
Yank the kill ring.
|
||||
.TP
|
||||
.B C-k \e
|
||||
Dump core.
|
||||
.B C-k C-x
|
||||
Save the file and exit.
|
||||
|
||||
.SS Other keybindings
|
||||
.TP
|
||||
.B C-g
|
||||
Cancel the current operation.
|
||||
.TP
|
||||
.B C-a
|
||||
Move to the beginning of the line.
|
||||
.TP
|
||||
.B C-e
|
||||
Move to the end of the line.
|
||||
.TP
|
||||
.B C-b
|
||||
Move left.
|
||||
.TP
|
||||
.B C-f
|
||||
Move right.
|
||||
.TP
|
||||
.B C-n
|
||||
Move down.
|
||||
.TP
|
||||
.B C-p
|
||||
Move up.
|
||||
.TP
|
||||
.B C-l
|
||||
Refresh the display.
|
||||
.TP
|
||||
.B C-d
|
||||
Delete the character at the cursor.
|
||||
.TP
|
||||
.B C-r
|
||||
Regex search.
|
||||
.TP
|
||||
.B C-s
|
||||
Incremental find.
|
||||
.TP
|
||||
.B C-t
|
||||
Regex search and replace.
|
||||
.TP
|
||||
.B C-h
|
||||
Search and replace.
|
||||
.TP
|
||||
.B C-u
|
||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||
.TP
|
||||
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
|
||||
.B C-y
|
||||
Yank the kill ring.
|
||||
.TP
|
||||
.B ESC <
|
||||
Move to the beginning of the file.
|
||||
.TP
|
||||
.B ESC >
|
||||
Move to the end of the file.
|
||||
.TP
|
||||
.B ESC m
|
||||
Toggle the mark.
|
||||
.TP
|
||||
.B ESC BACKSPACE
|
||||
Delete the previous word.
|
||||
.TP
|
||||
|
||||
100
docs/kte.1
100
docs/kte.1
@@ -1,7 +1,7 @@
|
||||
.\" kte(1) — Kyle's Text Editor (terminal-first)
|
||||
.\"
|
||||
.\" Project homepage: https://github.com/wntrmute/kte
|
||||
.TH KTE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
||||
.TH KTE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||
.SH NAME
|
||||
kte \- Kyle's Text Editor (terminal-first)
|
||||
.SH SYNOPSIS
|
||||
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
|
||||
.PP
|
||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||
.TP
|
||||
.B C-k BACKSPACE
|
||||
Delete from the cursor to the beginning of the line.
|
||||
.TP
|
||||
.B C-k SPACE
|
||||
Toggle the mark.
|
||||
.B C-k '
|
||||
Toggle read-only for the current buffer.
|
||||
.TP
|
||||
.B C-k -
|
||||
If the mark is set, unindent the region.
|
||||
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
|
||||
.B C-k =
|
||||
If the mark is set, indent the region.
|
||||
.TP
|
||||
.B C-k ;
|
||||
Open the generic command prompt (": ").
|
||||
.TP
|
||||
.B C-k a
|
||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||
.TP
|
||||
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
|
||||
Delete the entire line.
|
||||
.TP
|
||||
.B C-k e
|
||||
Edit a new file.
|
||||
Edit (open) a new file.
|
||||
.TP
|
||||
.B C-k f
|
||||
Flush the kill ring.
|
||||
@@ -93,14 +93,20 @@ Flush the kill ring.
|
||||
.B C-k g
|
||||
Go to a specific line.
|
||||
.TP
|
||||
.B C-k h
|
||||
Show the built-in help (+HELP+ buffer).
|
||||
.TP
|
||||
.B C-k j
|
||||
Jump to the mark.
|
||||
.TP
|
||||
.B C-k l
|
||||
Reload the current buffer from disk.
|
||||
.TP
|
||||
.B C-k m
|
||||
Run make(1), reporting success or failure.
|
||||
.B C-k n
|
||||
Switch to the previous buffer.
|
||||
.TP
|
||||
.B C-k o
|
||||
Change working directory (prompt).
|
||||
.TP
|
||||
.B C-k p
|
||||
Switch to the next buffer.
|
||||
@@ -111,14 +117,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
||||
.B C-k C-q
|
||||
Immediately exit the editor.
|
||||
.TP
|
||||
.B C-k r
|
||||
Redo changes.
|
||||
.TP
|
||||
.B C-k s
|
||||
Save the file, prompting for a filename if needed.
|
||||
.TP
|
||||
.B C-k u
|
||||
Undo.
|
||||
.TP
|
||||
.B C-k r
|
||||
Redo changes.
|
||||
.B C-k v
|
||||
Toggle visual file picker (GUI).
|
||||
.TP
|
||||
.B C-k w
|
||||
Show the current working directory.
|
||||
.TP
|
||||
.B C-k x
|
||||
Save the file and exit. Also C-k C-x.
|
||||
@@ -126,23 +138,76 @@ Save the file and exit. Also C-k C-x.
|
||||
.B C-k y
|
||||
Yank the kill ring.
|
||||
.TP
|
||||
.B C-k \e
|
||||
Dump core.
|
||||
.B C-k C-x
|
||||
Save the file and exit.
|
||||
|
||||
.SH GUI APPEARANCE
|
||||
When running the GUI frontend, you can control appearance via the generic
|
||||
command prompt (type "C-k ;" then enter commands):
|
||||
.TP
|
||||
.B : theme NAME
|
||||
Set the GUI theme. Available names: "nord", "gruvbox", "plan9", "solarized", "eink".
|
||||
Compatibility aliases are also accepted: "gruvbox-dark", "gruvbox-light",
|
||||
"solarized-dark", "solarized-light", "eink-dark", "eink-light".
|
||||
.TP
|
||||
.B : background MODE
|
||||
Set background mode for supported themes. MODE is either "light" or "dark".
|
||||
Themes that respond to background: eink, gruvbox, solarized. The
|
||||
"nord" and "plan9" themes do not vary with background.
|
||||
|
||||
.SH CONFIGURATION
|
||||
The GUI reads a simple configuration file at
|
||||
~/.config/kte/kge.ini. Recognized keys include:
|
||||
.IP "fullscreen=on|off"
|
||||
.IP "columns=NUM"
|
||||
.IP "rows=NUM"
|
||||
.IP "font_size=NUM"
|
||||
.IP "theme=NAME"
|
||||
.IP "background=light|dark"
|
||||
The theme name accepts the values listed above. The background key controls
|
||||
light/dark variants when the selected theme supports it.
|
||||
|
||||
.SS Other keybindings
|
||||
.TP
|
||||
.B C-g
|
||||
Cancel the current operation.
|
||||
.TP
|
||||
.B C-a
|
||||
Move to the beginning of the line.
|
||||
.TP
|
||||
.B C-e
|
||||
Move to the end of the line.
|
||||
.TP
|
||||
.B C-b
|
||||
Move left.
|
||||
.TP
|
||||
.B C-f
|
||||
Move right.
|
||||
.TP
|
||||
.B C-n
|
||||
Move down.
|
||||
.TP
|
||||
.B C-p
|
||||
Move up.
|
||||
.TP
|
||||
.B C-l
|
||||
Refresh the display.
|
||||
.TP
|
||||
.B C-d
|
||||
Delete the character at the cursor.
|
||||
.TP
|
||||
.B C-r
|
||||
Regex search.
|
||||
.TP
|
||||
.B C-s
|
||||
Incremental find.
|
||||
.TP
|
||||
.B C-t
|
||||
Regex search and replace.
|
||||
.TP
|
||||
.B C-h
|
||||
Search and replace.
|
||||
.TP
|
||||
.B C-u
|
||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||
.TP
|
||||
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
|
||||
.B C-y
|
||||
Yank the kill ring.
|
||||
.TP
|
||||
.B ESC <
|
||||
Move to the beginning of the file.
|
||||
.TP
|
||||
.B ESC >
|
||||
Move to the end of the file.
|
||||
.TP
|
||||
.B ESC m
|
||||
Toggle the mark.
|
||||
.TP
|
||||
.B ESC BACKSPACE
|
||||
Delete the previous word.
|
||||
.TP
|
||||
|
||||
525
docs/lsp plan.md
Normal file
525
docs/lsp plan.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# LSP Support Implementation Plan for kte
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This plan outlines a comprehensive approach to integrating Language Server Protocol (LSP) support into kte while
|
||||
respecting its core architectural principles: **frontend/backend separation**, **testability**, and **dual terminal/GUI
|
||||
support**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Architecture
|
||||
|
||||
### 1.1 LSP Client Module Structure
|
||||
|
||||
```c++
|
||||
// LspClient.h - Core LSP client abstraction
|
||||
class LspClient {
|
||||
public:
|
||||
virtual ~LspClient() = default;
|
||||
|
||||
// Lifecycle
|
||||
virtual bool initialize(const std::string& rootPath) = 0;
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
// Document Synchronization
|
||||
virtual void didOpen(const std::string& uri, const std::string& languageId,
|
||||
int version, const std::string& text) = 0;
|
||||
virtual void didChange(const std::string& uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent>& changes) = 0;
|
||||
virtual void didClose(const std::string& uri) = 0;
|
||||
virtual void didSave(const std::string& uri) = 0;
|
||||
|
||||
// Language Features
|
||||
virtual void completion(const std::string& uri, Position pos,
|
||||
CompletionCallback callback) = 0;
|
||||
virtual void hover(const std::string& uri, Position pos,
|
||||
HoverCallback callback) = 0;
|
||||
virtual void definition(const std::string& uri, Position pos,
|
||||
LocationCallback callback) = 0;
|
||||
virtual void references(const std::string& uri, Position pos,
|
||||
LocationsCallback callback) = 0;
|
||||
virtual void diagnostics(DiagnosticsCallback callback) = 0;
|
||||
|
||||
// Process Management
|
||||
virtual bool isRunning() const = 0;
|
||||
virtual std::string getServerName() const = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 1.2 Process-based LSP Implementation
|
||||
|
||||
```c++
|
||||
// LspProcessClient.h - Manages LSP server subprocess
|
||||
class LspProcessClient : public LspClient {
|
||||
private:
|
||||
std::string serverCommand_;
|
||||
std::vector<std::string> serverArgs_;
|
||||
std::unique_ptr<Process> process_;
|
||||
std::unique_ptr<JsonRpcTransport> transport_;
|
||||
std::unordered_map<int, PendingRequest> pendingRequests_;
|
||||
int nextRequestId_ = 1;
|
||||
|
||||
// Async I/O handling
|
||||
std::thread readerThread_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
|
||||
public:
|
||||
LspProcessClient(const std::string& command,
|
||||
const std::vector<std::string>& args);
|
||||
// ... implementation of LspClient interface
|
||||
};
|
||||
```
|
||||
|
||||
### 1.3 JSON-RPC Transport Layer
|
||||
|
||||
```c++
|
||||
// JsonRpcTransport.h
|
||||
class JsonRpcTransport {
|
||||
public:
|
||||
// Send a request and get the request ID
|
||||
int sendRequest(const std::string& method, const nlohmann::json& params);
|
||||
|
||||
// Send a notification (no response expected)
|
||||
void sendNotification(const std::string& method, const nlohmann::json& params);
|
||||
|
||||
// Read next message (blocking)
|
||||
std::optional<JsonRpcMessage> readMessage();
|
||||
|
||||
private:
|
||||
void writeMessage(const nlohmann::json& message);
|
||||
std::string readContentLength();
|
||||
|
||||
int fdIn_; // stdin to server
|
||||
int fdOut_; // stdout from server
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Incremental Document Updates
|
||||
|
||||
### 2.1 Change Tracking in Buffer
|
||||
|
||||
The key to efficient LSP integration is tracking changes incrementally. This integrates with the existing `Buffer`
|
||||
class:
|
||||
|
||||
```c++
|
||||
// TextDocumentContentChangeEvent.h
|
||||
struct TextDocumentContentChangeEvent {
|
||||
std::optional<Range> range; // If nullopt, entire document changed
|
||||
std::optional<int> rangeLength; // Deprecated but some servers use it
|
||||
std::string text;
|
||||
};
|
||||
|
||||
// BufferChangeTracker.h - Integrates with Buffer to track changes
|
||||
class BufferChangeTracker {
|
||||
public:
|
||||
explicit BufferChangeTracker(Buffer* buffer);
|
||||
|
||||
// Called by Buffer on each edit operation
|
||||
void recordInsertion(Position pos, const std::string& text);
|
||||
void recordDeletion(Range range, const std::string& deletedText);
|
||||
|
||||
// Get accumulated changes since last sync
|
||||
std::vector<TextDocumentContentChangeEvent> getChanges();
|
||||
|
||||
// Clear changes after sending to LSP
|
||||
void clearChanges();
|
||||
|
||||
// Get current document version
|
||||
int getVersion() const { return version_; }
|
||||
|
||||
private:
|
||||
Buffer* buffer_;
|
||||
int version_ = 0;
|
||||
std::vector<TextDocumentContentChangeEvent> pendingChanges_;
|
||||
|
||||
// Optional: Coalesce adjacent changes
|
||||
void coalesceChanges();
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 Integration with Buffer Operations
|
||||
|
||||
```c++
|
||||
// Buffer.h additions
|
||||
class Buffer {
|
||||
// ... existing code ...
|
||||
|
||||
// LSP integration
|
||||
void setChangeTracker(std::unique_ptr<BufferChangeTracker> tracker);
|
||||
BufferChangeTracker* getChangeTracker() { return changeTracker_.get(); }
|
||||
|
||||
// These methods should call tracker when present
|
||||
void insertText(Position pos, const std::string& text);
|
||||
void deleteRange(Range range);
|
||||
|
||||
private:
|
||||
std::unique_ptr<BufferChangeTracker> changeTracker_;
|
||||
};
|
||||
```
|
||||
|
||||
### 2.3 Sync Strategy Selection
|
||||
|
||||
```c++
|
||||
// LspSyncMode.h
|
||||
enum class LspSyncMode {
|
||||
None, // No sync
|
||||
Full, // Send full document on each change
|
||||
Incremental // Send only changes (preferred)
|
||||
};
|
||||
|
||||
// Determined during server capability negotiation
|
||||
LspSyncMode negotiateSyncMode(const ServerCapabilities& caps);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Diagnostics Display System
|
||||
|
||||
### 3.1 Diagnostic Data Model
|
||||
|
||||
```c++
|
||||
// Diagnostic.h
|
||||
enum class DiagnosticSeverity {
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Information = 3,
|
||||
Hint = 4
|
||||
};
|
||||
|
||||
struct Diagnostic {
|
||||
Range range;
|
||||
DiagnosticSeverity severity;
|
||||
std::optional<std::string> code;
|
||||
std::optional<std::string> source;
|
||||
std::string message;
|
||||
std::vector<DiagnosticRelatedInformation> relatedInfo;
|
||||
};
|
||||
|
||||
// DiagnosticStore.h - Central storage for diagnostics
|
||||
class DiagnosticStore {
|
||||
public:
|
||||
void setDiagnostics(const std::string& uri,
|
||||
std::vector<Diagnostic> diagnostics);
|
||||
const std::vector<Diagnostic>& getDiagnostics(const std::string& uri) const;
|
||||
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string& uri,
|
||||
int line) const;
|
||||
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string& uri,
|
||||
Position pos) const;
|
||||
void clear(const std::string& uri);
|
||||
void clearAll();
|
||||
|
||||
// Statistics
|
||||
int getErrorCount(const std::string& uri) const;
|
||||
int getWarningCount(const std::string& uri) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::vector<Diagnostic>> diagnostics_;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Frontend-Agnostic Diagnostic Display Interface
|
||||
|
||||
Following kte's existing abstraction pattern with `Frontend`, `Renderer`, and `InputHandler`:
|
||||
|
||||
```c++
|
||||
// DiagnosticDisplay.h - Abstract interface for showing diagnostics
|
||||
class DiagnosticDisplay {
|
||||
public:
|
||||
virtual ~DiagnosticDisplay() = default;
|
||||
|
||||
// Update the diagnostic indicators for a buffer
|
||||
virtual void updateDiagnostics(const std::string& uri,
|
||||
const std::vector<Diagnostic>& diagnostics) = 0;
|
||||
|
||||
// Show inline diagnostic at cursor position
|
||||
virtual void showInlineDiagnostic(const Diagnostic& diagnostic) = 0;
|
||||
|
||||
// Show diagnostic list/panel
|
||||
virtual void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) = 0;
|
||||
virtual void hideDiagnosticList() = 0;
|
||||
|
||||
// Status bar summary
|
||||
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 Terminal Diagnostic Display
|
||||
|
||||
```c++
|
||||
// TerminalDiagnosticDisplay.h
|
||||
class TerminalDiagnosticDisplay : public DiagnosticDisplay {
|
||||
public:
|
||||
explicit TerminalDiagnosticDisplay(TerminalRenderer* renderer);
|
||||
|
||||
void updateDiagnostics(const std::string& uri,
|
||||
const std::vector<Diagnostic>& diagnostics) override;
|
||||
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
|
||||
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
|
||||
void hideDiagnosticList() override;
|
||||
void updateStatusBar(int errorCount, int warningCount) override;
|
||||
|
||||
private:
|
||||
TerminalRenderer* renderer_;
|
||||
|
||||
// Terminal-specific display strategies
|
||||
void renderGutterMarkers(const std::vector<Diagnostic>& diagnostics);
|
||||
void renderUnderlines(const std::vector<Diagnostic>& diagnostics);
|
||||
void renderVirtualText(const Diagnostic& diagnostic);
|
||||
};
|
||||
```
|
||||
|
||||
**Terminal Display Strategies:**
|
||||
|
||||
1. **Gutter markers**: Show `E` (error), `W` (warning), `I` (info), `H` (hint) in left gutter
|
||||
2. **Underlines**: Use terminal underline/curly underline capabilities (where supported)
|
||||
3. **Virtual text**: Display diagnostic message at end of line (configurable)
|
||||
4. **Status line**: `[E:3 W:5]` summary
|
||||
5. **Message line**: Full diagnostic on cursor line shown in bottom bar
|
||||
|
||||
```
|
||||
1 │ fn main() {
|
||||
E 2 │ let x: i32 = "hello";
|
||||
3 │ }
|
||||
──────────────────────────────────────
|
||||
error[E0308]: mismatched types
|
||||
expected `i32`, found `&str`
|
||||
[E:1 W:0] main.rs
|
||||
```
|
||||
|
||||
### 3.4 GUI Diagnostic Display
|
||||
|
||||
```c++
|
||||
// GUIDiagnosticDisplay.h
|
||||
class GUIDiagnosticDisplay : public DiagnosticDisplay {
|
||||
public:
|
||||
explicit GUIDiagnosticDisplay(GUIRenderer* renderer, GUITheme* theme);
|
||||
|
||||
void updateDiagnostics(const std::string& uri,
|
||||
const std::vector<Diagnostic>& diagnostics) override;
|
||||
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
|
||||
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
|
||||
void hideDiagnosticList() override;
|
||||
void updateStatusBar(int errorCount, int warningCount) override;
|
||||
|
||||
private:
|
||||
GUIRenderer* renderer_;
|
||||
GUITheme* theme_;
|
||||
|
||||
// GUI-specific display
|
||||
void renderWavyUnderlines(const std::vector<Diagnostic>& diagnostics);
|
||||
void renderTooltip(Position pos, const Diagnostic& diagnostic);
|
||||
void renderDiagnosticPanel();
|
||||
};
|
||||
```
|
||||
|
||||
**GUI Display Features:**
|
||||
|
||||
1. **Wavy underlines**: Classic IDE-style (red for errors, yellow for warnings, etc.)
|
||||
2. **Gutter icons**: Colored icons/dots in the gutter
|
||||
3. **Hover tooltips**: Rich tooltips on hover showing full diagnostic
|
||||
4. **Diagnostic panel**: Bottom panel with clickable diagnostic list
|
||||
5. **Minimap markers**: Colored marks on the minimap (if present)
|
||||
|
||||
---
|
||||
|
||||
## 4. LspManager - Central Coordination
|
||||
|
||||
```c++
|
||||
// LspManager.h
|
||||
class LspManager {
|
||||
public:
|
||||
explicit LspManager(Editor* editor, DiagnosticDisplay* display);
|
||||
|
||||
// Server management
|
||||
void registerServer(const std::string& languageId,
|
||||
const LspServerConfig& config);
|
||||
bool startServerForBuffer(Buffer* buffer);
|
||||
void stopServer(const std::string& languageId);
|
||||
void stopAllServers();
|
||||
|
||||
// Document sync
|
||||
void onBufferOpened(Buffer* buffer);
|
||||
void onBufferChanged(Buffer* buffer);
|
||||
void onBufferClosed(Buffer* buffer);
|
||||
void onBufferSaved(Buffer* buffer);
|
||||
|
||||
// Feature requests
|
||||
void requestCompletion(Buffer* buffer, Position pos,
|
||||
CompletionCallback callback);
|
||||
void requestHover(Buffer* buffer, Position pos,
|
||||
HoverCallback callback);
|
||||
void requestDefinition(Buffer* buffer, Position pos,
|
||||
LocationCallback callback);
|
||||
|
||||
// Configuration
|
||||
void setDebugLogging(bool enabled);
|
||||
|
||||
private:
|
||||
Editor* editor_;
|
||||
DiagnosticDisplay* display_;
|
||||
DiagnosticStore diagnosticStore_;
|
||||
std::unordered_map<std::string, std::unique_ptr<LspClient>> servers_;
|
||||
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
|
||||
|
||||
void handleDiagnostics(const std::string& uri,
|
||||
const std::vector<Diagnostic>& diagnostics);
|
||||
std::string getLanguageId(Buffer* buffer);
|
||||
std::string getUri(Buffer* buffer);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration
|
||||
|
||||
```c++
|
||||
// LspServerConfig.h
|
||||
struct LspServerConfig {
|
||||
std::string command;
|
||||
std::vector<std::string> args;
|
||||
std::vector<std::string> filePatterns; // e.g., {"*.rs", "*.toml"}
|
||||
std::string rootPatterns; // e.g., "Cargo.toml"
|
||||
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
|
||||
bool autostart = true;
|
||||
std::unordered_map<std::string, nlohmann::json> initializationOptions;
|
||||
std::unordered_map<std::string, nlohmann::json> settings;
|
||||
};
|
||||
|
||||
// Default configurations
|
||||
std::vector<LspServerConfig> getDefaultServerConfigs() {
|
||||
return {
|
||||
{
|
||||
.command = "rust-analyzer",
|
||||
.filePatterns = {"*.rs"},
|
||||
.rootPatterns = "Cargo.toml"
|
||||
},
|
||||
{
|
||||
.command = "clangd",
|
||||
.args = {"--background-index"},
|
||||
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
|
||||
.rootPatterns = "compile_commands.json"
|
||||
},
|
||||
{
|
||||
.command = "gopls",
|
||||
.filePatterns = {"*.go"},
|
||||
.rootPatterns = "go.mod"
|
||||
},
|
||||
// ... more servers
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Phases
|
||||
|
||||
### Phase 1: Foundation (2-3 weeks)
|
||||
|
||||
- [ ] JSON-RPC transport layer
|
||||
- [ ] Process management for LSP servers
|
||||
- [ ] Basic `LspClient` with initialize/shutdown
|
||||
- [ ] `textDocument/didOpen`, `textDocument/didClose` (full sync)
|
||||
|
||||
### Phase 2: Incremental Sync (1-2 weeks)
|
||||
|
||||
- [ ] `BufferChangeTracker` integration with `Buffer`
|
||||
- [ ] `textDocument/didChange` with incremental updates
|
||||
- [ ] Change coalescing for rapid edits
|
||||
- [ ] Version tracking
|
||||
|
||||
### Phase 3: Diagnostics (2-3 weeks)
|
||||
|
||||
- [ ] `DiagnosticStore` implementation
|
||||
- [ ] `TerminalDiagnosticDisplay` with gutter markers & status line
|
||||
- [ ] `GUIDiagnosticDisplay` with wavy underlines & tooltips
|
||||
- [ ] `textDocument/publishDiagnostics` handling
|
||||
|
||||
### Phase 4: Language Features (3-4 weeks)
|
||||
|
||||
- [ ] Completion (`textDocument/completion`)
|
||||
- [ ] Hover (`textDocument/hover`)
|
||||
- [ ] Go to definition (`textDocument/definition`)
|
||||
- [ ] Find references (`textDocument/references`)
|
||||
- [ ] Code actions (`textDocument/codeAction`)
|
||||
|
||||
### Phase 5: Polish & Advanced Features (2-3 weeks)
|
||||
|
||||
- [ ] Multiple server support
|
||||
- [ ] Server auto-detection
|
||||
- [ ] Configuration file support
|
||||
- [ ] Workspace symbol search
|
||||
- [ ] Rename refactoring
|
||||
|
||||
---
|
||||
|
||||
## 7. Alignment with kte Core Principles
|
||||
|
||||
### 7.1 Frontend/Backend Separation
|
||||
|
||||
- LSP logic is completely separate from display
|
||||
- `DiagnosticDisplay` interface allows identical behavior across Terminal/GUI
|
||||
- Follows existing pattern: `Renderer`, `InputHandler`, `Frontend`
|
||||
|
||||
### 7.2 Testability
|
||||
|
||||
- `LspClient` is abstract, enabling `MockLspClient` for testing
|
||||
- `DiagnosticDisplay` can be mocked for testing diagnostic flow
|
||||
- Change tracking can be unit tested in isolation
|
||||
|
||||
### 7.3 Performance
|
||||
|
||||
- Incremental sync minimizes data sent to LSP servers
|
||||
- Async message handling doesn't block UI
|
||||
- Diagnostic rendering is batched
|
||||
|
||||
### 7.4 Simplicity
|
||||
|
||||
- Minimal dependencies (nlohmann/json for JSON handling)
|
||||
- Self-contained process management
|
||||
- Clear separation of concerns
|
||||
|
||||
---
|
||||
|
||||
## 8. File Organization
|
||||
|
||||
```
|
||||
kte/
|
||||
├── lsp/
|
||||
│ ├── LspClient.h
|
||||
│ ├── LspProcessClient.h
|
||||
│ ├── LspProcessClient.cc
|
||||
│ ├── LspManager.h
|
||||
│ ├── LspManager.cc
|
||||
│ ├── LspServerConfig.h
|
||||
│ ├── JsonRpcTransport.h
|
||||
│ ├── JsonRpcTransport.cc
|
||||
│ ├── LspTypes.h # Position, Range, Location, etc.
|
||||
│ ├── Diagnostic.h
|
||||
│ ├── DiagnosticStore.h
|
||||
│ ├── DiagnosticStore.cc
|
||||
│ └── BufferChangeTracker.h
|
||||
├── diagnostic/
|
||||
│ ├── DiagnosticDisplay.h
|
||||
│ ├── TerminalDiagnosticDisplay.h
|
||||
│ ├── TerminalDiagnosticDisplay.cc
|
||||
│ ├── GUIDiagnosticDisplay.h
|
||||
│ └── GUIDiagnosticDisplay.cc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Dependencies
|
||||
|
||||
- **nlohmann/json**: JSON parsing/serialization (header-only)
|
||||
- **POSIX/Windows process APIs**: For spawning LSP servers
|
||||
- Existing kte infrastructure: `Buffer`, `Renderer`, `Frontend`, etc.
|
||||
|
||||
---
|
||||
|
||||
This plan provides a solid foundation for LSP support while maintaining kte's clean architecture. The key insight is
|
||||
that LSP is fundamentally a backend feature that should be displayed through the existing frontend abstraction layer,
|
||||
ensuring consistent behavior across terminal and GUI modes.
|
||||
102
docs/syntax on.md
Normal file
102
docs/syntax on.md
Normal file
@@ -0,0 +1,102 @@
|
||||
### Objective
|
||||
Introduce fast, minimal‑dependency syntax highlighting to kte, consistent with current architecture (Editor/Buffer + GUI/Terminal renderers), preserving ke UX and performance.
|
||||
|
||||
### Guiding principles
|
||||
- Keep core small and fast; no heavy deps (C++17 only).
|
||||
- Start simple (stateless line regex), evolve incrementally (stateful, caching).
|
||||
- Work in both Terminal (ncurses) and GUI (ImGui) with consistent token classes and theme mapping.
|
||||
- Integrate without disrupting existing search highlight, selection, or cursor rendering.
|
||||
|
||||
### Scope of v1
|
||||
- Languages: plain text (off), C/C++ minimal set (keywords, types, strings, chars, comments, numbers, preprocessor).
|
||||
- Stateless per‑line highlighting; handle single‑line comments and strings; defer multi‑line state to v2.
|
||||
- Toggle: `:syntax on|off` and per‑buffer filetype selection.
|
||||
|
||||
### Architecture
|
||||
1. Core types (new):
|
||||
- `enum class TokenKind { Default, Keyword, Type, String, Char, Comment, Number, Preproc, Constant, Function, Operator, Punctuation, Identifier, Whitespace, Error };`
|
||||
- `struct HighlightSpan { int col_start; int col_end; TokenKind kind; };` // 0‑based columns in buffer indices per rendered line
|
||||
- `struct LineHighlight { std::vector<HighlightSpan> spans; uint64_t version; };`
|
||||
|
||||
2. Interfaces (new):
|
||||
- `class LanguageHighlighter { public: virtual ~LanguageHighlighter() = default; virtual void HighlightLine(const Buffer& buf, int row, std::vector<HighlightSpan>& out) const = 0; virtual bool Stateful() const { return false; } };`
|
||||
- `class HighlighterEngine { public: void SetHighlighter(std::unique_ptr<LanguageHighlighter>); const LineHighlight& GetLine(const Buffer&, int row, uint64_t buf_version); void InvalidateFrom(int row); };`
|
||||
- `class HighlighterRegistry { public: static const LanguageHighlighter& ForFiletype(std::string_view ft); static std::string DetectForPath(std::string_view path, std::string_view first_line); };`
|
||||
|
||||
3. Editor/Buffer integration:
|
||||
- Per‑Buffer settings: `bool syntax_enabled; std::string filetype; std::unique_ptr<HighlighterEngine> highlighter;`
|
||||
- Buffer emits a monotonically increasing `version` on edit; renderers request line highlights by `(row, version)`.
|
||||
- Invalidate cache minimally on edits (v1: current line only; v2: from current line down when stateful constructs present).
|
||||
|
||||
### Rendering integration
|
||||
- TerminalRenderer/GUIRenderer changes:
|
||||
- During line rendering, query `Editor.CurrentBuffer()->highlighter->GetLine(buf, row, buf_version)` to obtain spans.
|
||||
- Apply token styles while drawing glyph runs.
|
||||
- Z‑order and blending:
|
||||
1) Backgrounds (e.g., selection, search highlight rectangles)
|
||||
2) Text with syntax colors
|
||||
3) Cursor/IME decorations
|
||||
- Search highlights must remain visible over syntax colors:
|
||||
- Terminal: combine color/attr with reverse/bold for search; if color conflicts, prefer search.
|
||||
- GUI: draw semi‑transparent rects behind text (already present); keep syntax color for text.
|
||||
|
||||
### Theme and color mapping
|
||||
- Extend `GUITheme.h` with a `SyntaxPalette` mapping `TokenKind -> ImVec4 ink` (and optional background tint for comments/strings disabled by default). Provide default Light/Dark palettes.
|
||||
- Terminal: map `TokenKind` to ncurses color pairs where available; degrade gracefully on 8/16‑color terminals (e.g., comments=dim, keywords=bold, strings=yellow/green if available).
|
||||
|
||||
### Language detection
|
||||
- v1: by file extension; allow manual `:set filetype=<lang>`.
|
||||
- v2: add shebang detection for scripts, simple modelines (optional).
|
||||
|
||||
### Commands/UX
|
||||
- `:syntax on|off` — global default; buffer inherits on open.
|
||||
- `:set filetype=<lang>` — per‑buffer override.
|
||||
- `:syntax reload` — rebuild patterns/themes.
|
||||
- Status line shows filetype and syntax state when changed.
|
||||
|
||||
### Implementation plan (phased)
|
||||
1. Phase 1 — Minimal regex highlighter for C/C++
|
||||
- Implement `CppRegexHighlighter : LanguageHighlighter` with precompiled `std::regex` (or hand‑rolled simple scanners to avoid regex backtracking). Classes: line comment `//…`, block comment start `/*` (no state), string `"…"`, char `'…'` (no multiline), numbers, keywords/types, preprocessor `^\s*#\w+`.
|
||||
- Add `HighlighterEngine` with a simple per‑row cache keyed by `(row, buf_version)`; no background worker.
|
||||
- Integrate into both renderers; add palette to `GUITheme.h`; add terminal color selection.
|
||||
- Add commands.
|
||||
|
||||
2. Phase 2 — Stateful constructs and more languages
|
||||
- Add state machine for multiline comments `/*…*/` and multiline strings (C++11 raw strings), with invalidation from edit line downward until state stabilizes.
|
||||
- Add simple highlighters: JSON (strings, numbers, booleans, null, punctuation), Markdown (headers/emphasis/code fences), Shell (comments, strings, keywords), Go (types, constants, keywords), Python (strings, comments, keywords), Rust (strings, comments, keywords), Lisp (comments, strings, keywords),.
|
||||
- Filetype detection by extension + shebang.
|
||||
|
||||
3. Phase 3 — Performance and caching
|
||||
- Viewport‑first highlighting: compute only visible rows each frame; background task warms cache around viewport.
|
||||
- Reuse span buffers, avoid allocations; small‑vector optimization if needed.
|
||||
- Bench with large files; ensure O(n_visible) cost per frame.
|
||||
|
||||
4. Phase 4 — Extensibility
|
||||
- Public registration API for external highlighters.
|
||||
- Optional Tree‑sitter adapter behind a compile flag (off by default) to keep dependencies minimal.
|
||||
|
||||
### Data flow (per frame)
|
||||
- Renderer asks Editor for Buffer and viewport rows.
|
||||
- For each row: `engine.GetLine(buf, row, buf.version)` → spans.
|
||||
- Renderer emits runs with style from `SyntaxPalette[kind]`.
|
||||
- Search highlights are applied as separate background rectangles (GUI) or attribute toggles (Terminal), not overriding text color.
|
||||
|
||||
### Testing
|
||||
- Unit tests for tokenization per language: golden inputs → spans.
|
||||
- Fuzz/edge cases: escaped quotes, numeric literals, preprocessor lines.
|
||||
- Renderer tests with `TestRenderer` asserting the sequence of style changes for a line.
|
||||
- Performance tests: highlight 1k visible lines repeatedly; assert time under threshold.
|
||||
|
||||
### Risks and mitigations
|
||||
- Regex backtracking/perf: prefer linear scans; precompute keyword tables; avoid nested regex.
|
||||
- Terminal color limitations: feature‑detect colors; provide bold/dim fallbacks.
|
||||
- Stateful correctness: invalidate conservatively (from edit line downward) and cap work per frame.
|
||||
|
||||
### Deliverables
|
||||
- New files: `Highlight.h/.cc`, `HighlighterEngine.h/.cc`, `LanguageHighlighter.h`, `CppHighlighter.h/.cc`, optional `HighlighterRegistry.h/.cc`.
|
||||
- Renderer updates: `GUIRenderer.cc`, `TerminalRenderer.cc` to consume spans.
|
||||
- Theming: `GUITheme.h` additions for syntax colors.
|
||||
- Editor/Buffer: per‑buffer syntax settings and highlighter handle.
|
||||
- Commands in `Command.cc` and help text updates.
|
||||
- Docs: README/ROADMAP update and a brief `docs/syntax.md`.
|
||||
- Tests: unit and renderer golden tests.
|
||||
70
docs/syntax.md
Normal file
70
docs/syntax.md
Normal file
@@ -0,0 +1,70 @@
|
||||
Syntax highlighting in kte
|
||||
==========================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
kte provides lightweight syntax highlighting with a pluggable highlighter interface. The initial implementation targets C/C++ and focuses on speed and responsiveness.
|
||||
|
||||
Core types
|
||||
----------
|
||||
|
||||
- `TokenKind` — token categories (keywords, types, strings, comments, numbers, preprocessor, operators, punctuation, identifiers, whitespace, etc.).
|
||||
- `HighlightSpan` — a half-open column range `[col_start, col_end)` with a `TokenKind`.
|
||||
- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version` used to compute it.
|
||||
|
||||
Engine and caching
|
||||
------------------
|
||||
|
||||
- `HighlighterEngine` maintains a per-line cache of `LineHighlight` keyed by row and buffer version.
|
||||
- Cache invalidation occurs when the buffer version changes or when the buffer calls `InvalidateFrom(row)`, which clears cached lines and line states from `row` downward.
|
||||
- The engine supports both stateless and stateful highlighters. For stateful highlighters, it memoizes a simple per-line state and computes lines sequentially when necessary.
|
||||
|
||||
Stateful highlighters
|
||||
---------------------
|
||||
|
||||
- `LanguageHighlighter` is the base interface for stateless per-line tokenization.
|
||||
- `StatefulHighlighter` extends it with a `LineState` and the method `HighlightLineStateful(buf, row, prev_state, out)`.
|
||||
- The engine detects `StatefulHighlighter` via dynamic_cast and feeds each line the previous line’s state, caching the resulting state per line.
|
||||
|
||||
C/C++ highlighter
|
||||
-----------------
|
||||
|
||||
- `CppHighlighter` implements `StatefulHighlighter`.
|
||||
- Stateless constructs: line comments `//`, strings `"..."`, chars `'...'`, numbers, identifiers (keywords/types), preprocessor at beginning of line after leading whitespace, operators/punctuation, and whitespace.
|
||||
- Stateful constructs (v2):
|
||||
- Multi-line block comments `/* ... */` — the state records whether the next line continues a comment.
|
||||
- Raw strings `R"delim(... )delim"` — the state tracks whether we are inside a raw string and its delimiter `delim` until the closing sequence appears.
|
||||
|
||||
Limitations and TODOs
|
||||
---------------------
|
||||
|
||||
- Raw string detection is intentionally simple and does not handle all corner cases of the C++ standard.
|
||||
- Preprocessor handling is line-based; continuation lines with `\\` are not yet tracked.
|
||||
- No semantic analysis; identifiers are classified via small keyword/type sets.
|
||||
- Additional languages (JSON, Markdown, Shell, Python, Go, Rust, Lisp, …) are planned.
|
||||
- Terminal color mapping is conservative to support 8/16-color terminals. Rich color-pair themes can be added later.
|
||||
|
||||
Renderer integration
|
||||
--------------------
|
||||
|
||||
- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
|
||||
- Search highlight and cursor overlays take precedence over syntax colors.
|
||||
|
||||
Extensibility (Phase 4)
|
||||
-----------------------
|
||||
|
||||
- Public registration API: external code can register custom highlighters by filetype.
|
||||
- Use `HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
|
||||
- Registered factories are preferred over built-ins for the same filetype key.
|
||||
- Filetype keys are normalized via `HighlighterRegistry::Normalize()`.
|
||||
- Optional Tree-sitter adapter: disabled by default to keep dependencies minimal.
|
||||
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
|
||||
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if needed.
|
||||
- Register a Tree-sitter-backed highlighter for a language (example assumes you link a grammar):
|
||||
```c++
|
||||
extern "C" const TSLanguage* tree_sitter_c();
|
||||
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
|
||||
```
|
||||
- Current adapter is a stub scaffold; it compiles and integrates cleanly when enabled, but
|
||||
intentionally emits no spans until Tree-sitter node-to-token mapping is implemented.
|
||||
279
docs/undo-roadmap.md
Normal file
279
docs/undo-roadmap.md
Normal file
@@ -0,0 +1,279 @@
|
||||
Undo System Overhaul Roadmap (emacs-style undo-tree)
|
||||
|
||||
Context: macOS, C++17 project, ncurses terminal and SDL2/ImGui GUI frontends. Date: 2025-12-01.
|
||||
|
||||
Purpose
|
||||
|
||||
- Define a clear, incremental plan to implement a robust, non-linear undo system inspired by emacs' undo-tree.
|
||||
- Align implementation with docs/undo.md and fix gaps observed in docs/undo-state.md.
|
||||
- Provide test cases and acceptance criteria so a junior engineer or agentic coding system can execute the plan safely.
|
||||
|
||||
References
|
||||
|
||||
- Specification: docs/undo.md (API, invariants, batching rules, raw buffer ops)
|
||||
- Current snapshot and recent fix: docs/undo-state.md (GUI mapping notes; Begin/Append ordering fix)
|
||||
- Code: UndoSystem.{h,cc}, UndoTree.{h,cc}, UndoNode.{h,cc}, Buffer.{h,cc}, Command.{h,cc}, GUI/Terminal InputHandlers,
|
||||
KKeymap.
|
||||
|
||||
Instrumentation (KTE_UNDO_DEBUG)
|
||||
|
||||
- How to enable
|
||||
- Build with the CMake option `-DKTE_UNDO_DEBUG=ON` to enable concise instrumentation logs from `UndoSystem`.
|
||||
- The following targets receive the `KTE_UNDO_DEBUG` compile definition when ON:
|
||||
- `kte` (terminal), `kge` (GUI), and `test_undo` (tests).
|
||||
- Examples:
|
||||
```sh
|
||||
# Terminal build with tests and instrumentation ON
|
||||
cmake -S . -B cmake-build-term -DBUILD_TESTS=ON -DBUILD_GUI=OFF -DKTE_UNDO_DEBUG=ON
|
||||
cmake --build cmake-build-term --target test_undo -j
|
||||
./cmake-build-term/test_undo 2> undo.log
|
||||
|
||||
# GUI build (requires SDL2/OpenGL/Freetype toolchain) with instrumentation ON
|
||||
cmake -S . -B cmake-build-gui -DBUILD_GUI=ON -DKTE_UNDO_DEBUG=ON
|
||||
cmake --build cmake-build-gui --target kge -j
|
||||
# Run kge and perform actions; logs go to stderr
|
||||
```
|
||||
|
||||
- What it logs
|
||||
- Each Begin/Append/commit/undo/redo operation prints a single `[UNDO]` line with:
|
||||
- current cursor `(row,col)`, pointer to `pending`, its type/row/col/text-size, and pointers to `current`/`saved`.
|
||||
- Example fields: `[UNDO] Begin cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=2 current=0x... saved=0x...`
|
||||
|
||||
- Example trace snippets
|
||||
- Typing a contiguous word ("Hello") batches into a single Insert node; one commit occurs before the subsequent undo:
|
||||
```text
|
||||
[UNDO] Begin cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||
[UNDO] commit:enter cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||
[UNDO] Begin:new cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=0 current=0x0 saved=0x0
|
||||
[UNDO] Append:sv cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=1 current=0x0 saved=0x0
|
||||
... (more Append as characters are typed) ...
|
||||
[UNDO] commit:enter cur=(0,5) pending=0x... t=Insert r=0 c=0 nlen=5 current=0x0 saved=0x0
|
||||
[UNDO] commit:done cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||
```
|
||||
|
||||
- Undo then Redo across that batch:
|
||||
```text
|
||||
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||
[UNDO] undo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||
[UNDO] redo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||
```
|
||||
|
||||
- Newline and backspace/delete traces follow the same pattern with `t=Newline` or `t=Delete` and immediate commit for newline.
|
||||
Capture by running `kge`/`kte` with `KTE_UNDO_DEBUG=ON` and performing the actions; append representative 3–6 line snippets to docs.
|
||||
|
||||
Notes
|
||||
|
||||
- Pointer values and exact cursor positions in the logs depend on the runtime and actions; this is expected.
|
||||
- Keep `KTE_UNDO_DEBUG` OFF by default in CI/release to avoid noisy logs and any performance impact.
|
||||
|
||||
̄1) Current State Summary (from docs/undo-state.md)
|
||||
|
||||
- Terminal (kte): Keybindings and UndoSystem integration have been stable.
|
||||
- GUI (kge): Previously, C-k u/U mapping and SDL TEXTINPUT suppression had issues on macOS; these were debugged. The
|
||||
core root cause of “status shows Undone but no change” was fixed by moving UndoSystem::Begin/Append/commit to occur
|
||||
after buffer modifications/cursor updates so batching conditions see the correct cursor.
|
||||
- Undo core exists with tree invariants, saved marker/dirty flag mirroring, batching for Insert/Delete, and Newline as a
|
||||
single-step undo.
|
||||
|
||||
Gaps/Risks
|
||||
|
||||
- Event-path unification between KEYDOWN and TEXTINPUT across platforms (macOS specifics).
|
||||
- Comprehensive tests for branching, GC/limits, multi-line operations, and UTF-8 text input.
|
||||
- Advanced/compound command grouping and future region operations.
|
||||
|
||||
|
||||
2) Design Goals (emacs-like undo-tree)
|
||||
|
||||
- Per-buffer, non-linear undo tree: new edits after undo create a branch; existing redo branches are discarded.
|
||||
- Batching: insert/backspace/paste/newline grouped into sensible units to match user expectations.
|
||||
- Silent apply during undo/redo (no re-recording), using raw Buffer methods only.
|
||||
- Correct saved/dirty tracking and robust pending node lifecycle (detached until commit).
|
||||
- Efficient memory behavior; optional pruning limits similar to emacs (undo-limit, undo-strong-limit).
|
||||
- Deterministic behavior across terminal and GUI frontends.
|
||||
|
||||
|
||||
3) Invariants and API (must align with docs/undo.md)
|
||||
|
||||
- UndoTree holds root/current/saved/pending; pending is detached and only linked on commit.
|
||||
- Begin(type) reuses pending only if: same type, same row, and pending->col + pending->text.size() == current cursor
|
||||
col (or prepend rules for backspace sequences); otherwise it commits and starts a new node.
|
||||
- commit(): frees redo siblings from current, attaches pending as current->child, advances current, clears pending;
|
||||
nullifies saved marker if diverged.
|
||||
- undo()/redo(): move current and apply the node using low-level Buffer APIs that do not trigger undo recording.
|
||||
- mark_saved(): updates saved pointer and dirty flag (dirty ⇔ current != saved).
|
||||
- discard_pending()/clear(): lifecycle for buffer close/reset/new file.
|
||||
|
||||
|
||||
4) Phased Roadmap
|
||||
|
||||
Phase 0 — Baseline & Instrumentation (1 day)
|
||||
|
||||
- Audit UndoSystem against docs/undo.md invariants; ensure apply() uses only raw Buffer ops.
|
||||
- Verify Begin/Append ordering across all edit commands: insert, backspace, delete, newline, paste.
|
||||
- Add a temporary debug toggle (compile-time or editor flag) to log Begin/Append/commit/undo/redo, cursor(row,col), node
|
||||
sizes, and pending state. Include assertions for: pending detached, commit clears pending, redo branch freed on new
|
||||
commit, and correct batching preconditions.
|
||||
- Deliverables: Short log from typing/undo/redo scenarios; instrumentation behind a macro so it can be removed.
|
||||
|
||||
Phase 1 — Input Path Unification & Batching Rules (1–2 days)
|
||||
|
||||
- Ensure all printable text insertion (terminal and GUI) flows through CommandId::InsertText and reaches UndoSystem
|
||||
Begin/Append. On SDL, handle KEYDOWN vs TEXTINPUT consistently; always suppress trailing TEXTINPUT after k-prefix
|
||||
suffix commands.
|
||||
- Commit boundaries: at k-prefix entry, before Undo/Redo, on cursor movement, on focus/file ops, and before any
|
||||
non-editing command that should separate undo units.
|
||||
- Batching heuristics:
|
||||
- Insert: same row, contiguous columns; Append(std::string_view) handles multi-character text (pastes, IME).
|
||||
- Backspace: prepend batching in increasing column order (store deleted text in forward order).
|
||||
- Delete (forward): contiguous at same row/col.
|
||||
- Newline: record as UndoType::Newline and immediately commit (single-step undo for line splits/joins).
|
||||
- Deliverables: Manual tests pass for typing/backspace/delete/newline/paste; GUI C-k u/U work as expected on macOS.
|
||||
|
||||
Phase 2 — Tree Limits & GC (1 day)
|
||||
|
||||
- Add configurable memory/size limits for undo data (soft and strong limits like emacs). Implement pruning of oldest
|
||||
ancestors or deep redo branches while preserving recent edits. Provide stats (node count, bytes in text storage).
|
||||
- Deliverables: Config hooks, tests demonstrating pruning without violating apply/undo invariants.
|
||||
|
||||
Phase 3 — Compound Commands & Region Ops (2–3 days)
|
||||
|
||||
- Introduce an optional RAII-style UndoTransaction to group multi-step commands (indent region, kill region, rectangle
|
||||
ops) into a single undo step. Internally this just sequences Begin/Append and ensures commit even on early returns.
|
||||
- Support row operations (InsertRow/DeleteRow) with proper raw Buffer calls. Ensure join_lines/split_line are handled by
|
||||
Newline nodes or dedicated types if necessary.
|
||||
- Deliverables: Commands updated to use transactions when appropriate; tests for region delete/indent and multi-line
|
||||
paste.
|
||||
|
||||
Phase 4 — Developer UX & Diagnostics (1 day)
|
||||
|
||||
- Add a dev command to dump the undo tree (preorder) with markers for current/saved and pending (detached). For GUI,
|
||||
optionally expose a simple ImGui debug window (behind a compile flag) that visualizes the current branch.
|
||||
- Editor status improvements: show short status codes for undo/redo and when a new branch was created or redo discarded.
|
||||
- Deliverables: Tree dump command; example output in docs.
|
||||
|
||||
Phase 5 — Comprehensive Tests & Property Checks (2–3 days)
|
||||
|
||||
- Unit tests (extend test_undo.cc):
|
||||
- Insert batching: type "Hello" then one undo removes all; redo restores.
|
||||
- Backspace batching: type "Hello", backspace 3×, undo → restores the 3; redo → re-applies deletion.
|
||||
- Delete batching (forward delete) with cursor not moving.
|
||||
- Newline: split a line and undo to join; join a line (via backspace at col 0) and undo to split.
|
||||
- Branching: type "abc", undo twice, type "X" → redo history discarded; ensure redo no longer restores 'b'/'c'.
|
||||
- Saved/dirty: mark_saved after typing; ensure dirty flag toggles correctly after undo/redo; saved marker tracks the
|
||||
node.
|
||||
- discard_pending: create pending by typing, then move cursor or invoke commit boundary; ensure pending is attached;
|
||||
also ensure discard on buffer close clears pending.
|
||||
- clear(): resets state with no leaks; tree pointers null.
|
||||
- UTF-8 input: insert multi-byte characters via InsertText with multi-char std::string; ensure counts/col tracking
|
||||
behave (text stored as bytes; editor col policy consistent within kte).
|
||||
- Integration tests (TestFrontend):
|
||||
- Both TerminalFrontend and GUIFrontend: simulate text input and commands, including k-prefix C-k u/U.
|
||||
- Paste scenarios: multi-character insertions batched as one.
|
||||
- Property tests (optional but recommended):
|
||||
- Generate random sequences of edits; record them; then apply undo until root and redo back to the end → buffer
|
||||
contents match at each step; no crashes; dirty flag transitions consistent. Seed-based to reproduce failures.
|
||||
- Redo-branch discard property: any new edit after undo must eliminate redo path; redoing should be impossible
|
||||
afterward.
|
||||
- Deliverables: Tests merged and passing on CI for both frontends; failures block changes to undo core.
|
||||
|
||||
Phase 6 — Performance & Stress (0.5–1 day)
|
||||
|
||||
- Stress test with large files and long edit sequences. Target: smooth typing at 10k+ ops/minute on commodity hardware;
|
||||
memory growth bounded when GC limits enabled.
|
||||
- Deliverables: Basic perf notes; optional lightweight benchmarks.
|
||||
|
||||
|
||||
5) Acceptance Criteria
|
||||
|
||||
- Conformance to docs/undo.md invariants and API surface (including raw Buffer operations for apply()).
|
||||
- Repro checklist passes:
|
||||
- Type text; single-step undo/redo works and respects batching.
|
||||
- Backspace/delete batching works.
|
||||
- Newline split/join are single-step undo/redo.
|
||||
- Branching works: undo, then type → redo path is discarded; no ghost redo.
|
||||
- Saved/dirty flags accurate across undo/redo and diverge/rejoin paths.
|
||||
- No pending nodes leaked on buffer close/reload; no re-recording during undo/redo.
|
||||
- Behavior identical across terminal and GUI input paths.
|
||||
- Tests added for all above; CI green.
|
||||
|
||||
|
||||
6) Concrete Work Items by File
|
||||
|
||||
- UndoSystem.h/cc:
|
||||
- Re-verify Begin/Append ordering; enforce batching invariants; prepend logic for backspace; immediate commit for
|
||||
newline.
|
||||
- Implement/verify apply() uses only Buffer raw methods: insert_text/delete_text/split_line/join_lines/row ops.
|
||||
- Add limits (configurable) and stats; add discard_pending safety paths.
|
||||
- Buffer.h/cc:
|
||||
- Ensure raw methods exist and do not trigger undo; ensure UpdateBufferReference is correctly used when
|
||||
replacing/renaming the underlying buffer.
|
||||
- Call undo.commit() on cursor movement and non-editing commands (via Command layer integration).
|
||||
- Command.cc:
|
||||
- Ensure all edit commands drive UndoSystem correctly; commit at k-prefix entry and before Undo/Redo.
|
||||
- Introduce UndoTransaction for compound commands when needed.
|
||||
- GUIInputHandler.cc / TerminalInputHandler.cc / KKeymap.cc:
|
||||
- Ensure unified InsertText path; suppress SDL_TEXTINPUT when a k-prefix suffix produced a command; preserve case
|
||||
mapping.
|
||||
- Tests: test_undo.cc (extend) + new tests (e.g., test_undo_branching.cc, test_undo_multiline.cc).
|
||||
|
||||
|
||||
7) Example Test Cases (sketches)
|
||||
|
||||
- Branch discard after undo:
|
||||
1) InsertText("abc"); Undo(); Undo(); InsertText("X"); Redo();
|
||||
Expected: Redo is a no-op (or status indicates no redo), buffer is "aX".
|
||||
|
||||
- Newline split/join:
|
||||
1) InsertText("ab"); Newline(); InsertText("c"); Undo();
|
||||
Expected: single undo joins lines → buffer "abc" on one line at original join point; Redo() splits again.
|
||||
|
||||
- Backspace batching:
|
||||
1) InsertText("hello"); Backspace×3; Undo();
|
||||
Expected: restores "hello".
|
||||
|
||||
- UTF-8 insertion:
|
||||
1) InsertText("😀汉"); Undo(); Redo();
|
||||
Expected: content unchanged across cycles; no crashes.
|
||||
|
||||
- Saved/dirty transitions:
|
||||
1) InsertText("hi"); mark_saved(); InsertText("!"); Undo(); Redo();
|
||||
Expected: dirty false after mark_saved; dirty true after InsertText("!"); dirty returns to false after Undo();
|
||||
true again after Redo().
|
||||
|
||||
|
||||
8) Risks & Mitigations
|
||||
|
||||
- SDL/macOS event ordering (KEYDOWN vs TEXTINPUT, IME): Mitigate by suppressing TEXTINPUT on mapped k-prefix suffixes;
|
||||
optionally temporarily disable SDL text input during k-prefix suffix mapping; add targeted diagnostics.
|
||||
- UTF-8 width vs byte-length: Store bytes in UndoNode::text; keep column logic consistent with existing Buffer
|
||||
semantics.
|
||||
- Memory growth: Add GC/limits and provide a way to clear/reduce history for huge sessions.
|
||||
- Re-entrancy during apply(): Prevent public edit paths from being called; use only raw operations.
|
||||
|
||||
|
||||
9) Nice-to-Have (post-MVP)
|
||||
|
||||
- Visual undo-tree navigation (emacs-like time travel and branch selection), at least as a debug tool initially.
|
||||
- Persistent undo across saves (opt-in; likely out-of-scope initially).
|
||||
- Time-based batching threshold (e.g., break batches after >500ms pause in typing).
|
||||
|
||||
|
||||
10) Execution Notes for a Junior Engineer/Agentic System
|
||||
|
||||
- Start from Phase 0; do not skip instrumentation—assertions will catch subtle batching bugs early.
|
||||
- Change one surface at a time; when adjusting Begin/Append/commit positions, re-run unit tests immediately.
|
||||
- Always ensure commit boundaries before invoking commands that move the cursor/state.
|
||||
- When unsure about apply(), read docs/undo.md and mirror exactly: only raw Buffer methods, never the public editing
|
||||
APIs.
|
||||
- Keep diffs small and localized; add tests alongside behavior changes.
|
||||
|
||||
Appendix A — Minimal Developer Checklist
|
||||
|
||||
- [ ] Begin/Append occur after buffer mutation and cursor updates for all edit commands.
|
||||
- [ ] Pending detached until commit; freed/cleared on commit/discard/clear.
|
||||
- [ ] Redo branches freed on new commit after undo.
|
||||
- [ ] mark_saved updates both saved pointer and dirty flag; dirty mirrors current != saved.
|
||||
- [ ] apply() uses only raw Buffer methods; no recording during apply.
|
||||
- [ ] Terminal and GUI both route printable input to InsertText; k-prefix mapping suppresses trailing TEXTINPUT.
|
||||
- [ ] Unit and integration tests cover batching, branching, newline, saved/dirty, and UTF-8 cases.
|
||||
@@ -1,128 +1,139 @@
|
||||
Undo/Redo + C-k GUI status (macOS) — current state snapshot
|
||||
### Context recap
|
||||
|
||||
Context
|
||||
- Platform: macOS (Darwin)
|
||||
- Target: GUI build (kge) using SDL2/ImGui path
|
||||
- Date: 2025-11-30 00:30 local (from user)
|
||||
- The undo system is now tree‑based with batching rules and `KTE_UNDO_DEBUG` instrumentation hooks already present in
|
||||
`UndoSystem.{h,cc}`.
|
||||
- GUI path uses SDL; printable input now flows exclusively via `SDL_TEXTINPUT` to `CommandId::InsertText`, while
|
||||
control/meta/movement (incl. Backspace/Delete/Newline and k‑prefix) come from `SDL_KEYDOWN`.
|
||||
- Commit boundaries must be enforced at well‑defined points (movement, non‑editing commands, newline, undo/redo, etc.).
|
||||
|
||||
What works right now
|
||||
- Terminal (kte): C-k keymap and UndoSystem integration have been stable in recent builds.
|
||||
- GUI: Most C-k mappings work: C-k d (KillToEOL), C-k x (Save+Quit), C-k q (Quit) — confirmed by user.
|
||||
- UndoSystem core is implemented and integrated for InsertText/Newline/Delete/Backspace. Buffer owns an UndoSystem and raw edit APIs are used by apply().
|
||||
### Status summary (2025‑12‑01)
|
||||
|
||||
What is broken (GUI, macOS)
|
||||
- C-k u: Status shows "Undone" but buffer content does not change (no visible undo).
|
||||
- C-k U: Inserts a literal 'U' into the buffer; does not execute Redo.
|
||||
- C-k C-u / C-k C-U: No effect (expected unmapped), but the k-prefix prompt can remain in some paths.
|
||||
- Input‑path unification: Completed. `GUIInputHandler.cc` routes all printable characters through `SDL_TEXTINPUT → InsertText`.
|
||||
Newlines originate only from `SDL_KEYDOWN → Newline`. CR/LF are filtered out of `SDL_TEXTINPUT` payloads. Suppression
|
||||
rules prevent stray `TEXTINPUT` after meta/prefix/universal‑argument flows. Terminal input path remains consistent.
|
||||
- Tests: `test_undo.cc` expanded to cover branching behavior, UTF‑8 insertion, multi‑line newline/join, and typing batching.
|
||||
All scenarios pass.
|
||||
- Instrumentation: `KTE_UNDO_DEBUG` hooks exist in `UndoSystem.{h,cc}`; a CMake toggle has not yet been added.
|
||||
- Commit boundaries: Undo/Redo commit boundaries are in place; newline path commits immediately by design. A final audit
|
||||
pass across movement/non‑editing commands is still pending.
|
||||
- Docs: This status document updated. Further docs (instrumentation how‑to and example traces) remain pending in
|
||||
`docs/undo.md` / `docs/undo-roadmap.md`.
|
||||
|
||||
Repro steps (GUI)
|
||||
1) Type "Hello".
|
||||
2) Press C-k then press u → status becomes "Undone", but text remains "Hello".
|
||||
3) Press C-k then press Shift+U → a literal 'U' is inserted (becomes "HelloU").
|
||||
4) Press C-k then hold Ctrl on the suffix and press u → status "Undone", still no change.
|
||||
5) Press C-k then hold Ctrl on the suffix and press Shift+U → status shows the k-prefix prompt again ("C-k _").
|
||||
### Objectives
|
||||
|
||||
Keymap and input-layer changes we attempted (and kept)
|
||||
- KKeymap.cc: Case-sensitive 'U' mapping prioritized before the lowercase table. Added ctrl→non-ctrl fall-through so C-k u/U still map even if SDL reports Ctrl held on the suffix.
|
||||
- TerminalInputHandler: already preserved case and mapped correctly.
|
||||
- GUIInputHandler:
|
||||
- Preserve case for k-prefix suffix letters (Shift → uppercase). Clear esc_meta before k-suffix mapping.
|
||||
- Strengthened SDL_TEXTINPUT suppression after a k-prefix printable suffix to avoid inserting literal characters.
|
||||
- Added fallback to map the k-prefix suffix in the SDL_TEXTINPUT path (to catch macOS cases where uppercase arrives in TEXTINPUT rather than KEYDOWN).
|
||||
- Fixed malformed switch block introduced during iteration.
|
||||
- Command layer: commit pending undo batch at k-prefix entry and just before Undo/Redo so prior typing can actually be undone/redone.
|
||||
- Use the existing instrumentation to capture short traces of typing/backspacing/deleting and undo/redo.
|
||||
- Unify input paths (SDL `KEYDOWN` vs `TEXTINPUT`) and lock down commit boundaries across commands.
|
||||
- Extend tests to cover branching behavior, UTF‑8, and multi‑line operations.
|
||||
|
||||
Diagnostics added
|
||||
- GUIInputHandler logs k-prefix u/U suffix attempts to stderr and (previously) /tmp/kge.log. The user’s macOS session showed only KEYDOWN logs for 'u':
|
||||
- "[kge] k-prefix suffix: sym=117 mods=0x0 ascii=117 'u' ctrl2=0 pass_ctrl=0 mapped=1 id=38"
|
||||
- "[kge] k-prefix suffix: sym=117 mods=0x80 ascii=117 'u' ctrl2=1 pass_ctrl=0 mapped=1 id=38"
|
||||
- No logs were produced for 'U' (neither KEYDOWN nor TEXTINPUT). The /tmp log file was not created on the user’s host in the last run (stderr logs were visible earlier from KEYDOWN).
|
||||
### Plan of action
|
||||
|
||||
Hypotheses for current failures
|
||||
1) Undo appears to be invoked (status "Undone"), but no state change:
|
||||
- The most likely cause is that no committed node exists at the time of undo (i.e., typing "Hello" is not being recorded as an undo node), because our current typing path in Command.cc directly edits buffer rows without always driving UndoSystem Begin/Append/commit at the right times for every printable char on GUI.
|
||||
- Although we call u->Begin(Insert) and u->Append(text) in cmd_insert_text for CommandId::InsertText, the GUI printable input might be arriving through a different path or being short-circuited (e.g., via a prompt or suppression), resulting in actual text insertion but no corresponding UndoSystem pending node content, or pending but never committed.
|
||||
- We now commit at k-prefix entry and before undo; if there is still "nothing to undo", it implies the batch never had text appended (Append not called) or is detached from the real buffer edits.
|
||||
1. Enable instrumentation and make it easy to toggle
|
||||
- Add a CMake option in `CMakeLists.txt` (root project):
|
||||
`option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)`.
|
||||
- When ON, add a compile definition `-DKTE_UNDO_DEBUG` to all targets that include the editor core (e.g., `kte`,
|
||||
`kge`, and test binaries).
|
||||
- Keep the default OFF so normal builds are quiet; ensure both modes compile in CI.
|
||||
|
||||
2) Redo via C-k U inserts a literal 'U':
|
||||
- On macOS, uppercase letters often arrive as SDL_TEXTINPUT events. We added TEXTINPUT-based k-prefix mapping, but the user's run still showed a literal insertion and no diagnostic lines for TEXTINPUT, which suggests:
|
||||
a) The TEXTINPUT suppression didn’t trigger for that platform/sequence, or
|
||||
b) The k-prefix flag was already cleared by the time TEXTINPUT arrived, so the TEXTINPUT path defaulted to InsertText, or
|
||||
c) The GUI window’s input focus or SDL event ordering differs from expectations (e.g., IME/text input settings), so our suppression/mapping didn’t see the event.
|
||||
2. Capture short traces to validate current behavior
|
||||
- Build with `-DKTE_UNDO_DEBUG=ON` and run the GUI frontend:
|
||||
- Scenario A: type a contiguous word, then move cursor (should show `Begin(Insert)` + multiple `Append`, single
|
||||
`commit` at a movement boundary).
|
||||
- Scenario B: hold backspace to delete a run, including backspace batching (prepend rule); verify
|
||||
`Begin(Delete)` with prepended `Append` behavior, single `commit`.
|
||||
- Scenario C: forward deletes at a fixed column (anchor batching); expected single `Begin(Delete)` with same
|
||||
column.
|
||||
- Scenario D: insert newline (`Newline` node) and immediately commit; type text on the next line; undo/redo
|
||||
across the boundary.
|
||||
- Scenario E: undo chain and redo chain; then type new text and confirm redo branch gets discarded in logs.
|
||||
- Save representative trace snippets and add to `docs/undo.md` or `docs/undo-roadmap.md` for reference.
|
||||
|
||||
Relevant code pointers
|
||||
- Key mapping tables: KKeymap.cc → KLookupKCommand() (C-k suffix), KLookupCtrlCommand(), KLookupEscCommand().
|
||||
- Terminal input: TerminalInputHandler.cc → map_key_to_command().
|
||||
- GUI input: GUIInputHandler.cc → map_key() and GUIInputHandler::ProcessSDLEvent() (KEYDOWN + TEXTINPUT handling, suppression, k_prefix_/esc_meta_ flags).
|
||||
- Command dispatch: Command.cc → cmd_insert_text(), cmd_newline(), cmd_backspace(), cmd_delete_char(), cmd_undo(), cmd_redo(), cmd_kprefix().
|
||||
- Undo core: UndoSystem.{h,cc}, UndoNode.{h,cc}, UndoTree.{h,cc}. Buffer raw methods used by apply().
|
||||
3. Input‑path unification (SDL `KEYDOWN` vs `TEXTINPUT`) — Completed 2025‑12‑01
|
||||
- In `GUIInputHandler.cc`:
|
||||
- Ensure printable characters are generated exclusively from `SDL_TEXTINPUT` and mapped to
|
||||
`CommandId::InsertText`.
|
||||
- Keep `SDL_KEYDOWN` for control/meta/movement, backspace/delete, newline, and k‑prefix handling.
|
||||
- Maintain suppression of stray `SDL_TEXTINPUT` immediately following meta/prefix or universal‑argument
|
||||
collection so no accidental text is inserted.
|
||||
- Confirm that `InsertText` path never carries `"\n"`; newline must only originate from `KEYDOWN` →
|
||||
`CommandId::Newline`.
|
||||
- If the terminal input path exists, ensure parity: printable insertions go through `InsertText`, control via key
|
||||
events, and the same commit boundaries apply.
|
||||
- Status: Implemented. See `GUIInputHandler.cc` changes; tests confirm parity with terminal path.
|
||||
|
||||
Immediate next steps (when we return to this)
|
||||
1) Verify that GUI printable insertion always flows through CommandId::InsertText so UndoSystem::Begin/Append gets called. If SDL_TEXTINPUT delivers multi-byte strings, ensure Append() is given the same text inserted into buffer.
|
||||
- Add a one-session debug hook in cmd_insert_text to assert/trace: pending node type/text length and current cursor col before/after.
|
||||
- If GUI sometimes sends CommandId::InsertTextEmpty or another path, unify.
|
||||
4. Enforce and verify commit boundaries in command execution — In progress
|
||||
- Audit `Command.cc` and ensure `u->commit()` is called before executing any non‑editing command that should end a
|
||||
batch:
|
||||
- Movement commands (left/right/up/down/home/end/page).
|
||||
- Prompt accept/cancel transitions and mode switches (search prompts, replace prompts).
|
||||
- Buffer/file operations (open/switch/save/close), and focus changes.
|
||||
- Before running `Undo` or `Redo` (already present).
|
||||
- Ensure immediate commit at the end of atomic edit operations:
|
||||
- `Newline` insertion and line joins (`Delete` of newline when backspacing at column 0) should create
|
||||
`UndoType::Newline` and commit immediately (parts are already implemented; verify all call sites).
|
||||
- Pastes should be a single `Paste`/`Insert` batch per operation (depending on current design).
|
||||
|
||||
2) Ensure batching rules are satisfied so Begin() reuses pending correctly:
|
||||
- Begin(Insert) must see same row and col == pending->col + pending->text.size() for typing sequences.
|
||||
- If GUI accumulates multiple characters per TEXTINPUT (e.g., pasted text), Append(std::string_view) is fine, but row/col expectations remain.
|
||||
5. Extend automated tests (or add them if absent) — Phase 1 completed
|
||||
- Branching behavior ✓
|
||||
- Insert `"abc"`, undo twice (back to `"a"`), insert `"X"`, assert redo list is discarded, and new timeline
|
||||
continues with `aX`.
|
||||
- Navigate undo/redo along the new branch to ensure correctness.
|
||||
- UTF‑8 insertion and deletion ✓
|
||||
- Insert `"é漢"` (multi‑byte characters) via `InsertText`; verify buffer content and that a single Insert batch
|
||||
is created.
|
||||
- Undo/redo restores/removes the full insertion batch.
|
||||
- Backspace after typed UTF‑8 should remove the last inserted codepoint from the batch in a single undo step (
|
||||
current semantics are byte‑oriented in buffer ops; test to current behavior and document).
|
||||
- Multi‑line operations ✓
|
||||
- Newline splits a line: verify an `UndoType::Newline` node is created and committed immediately; undo/redo
|
||||
round‑trip.
|
||||
- Backspace at column 0 joins with previous line: record as `Newline` deletion (via `UndoType::Newline`
|
||||
inverse); undo/redo round‑trip.
|
||||
- Typing and deletion batching ✓ (typing) / Pending (delete batching)
|
||||
- Typing a contiguous word (no cursor moves) yields one `Insert` node with accumulated text.
|
||||
- Forward delete at a fixed anchor column yields one `Delete` batch. (Pending test)
|
||||
- Backspace batching uses the prepend rule when the cursor moves left. (Pending test)
|
||||
- Place tests near existing test suite files (e.g., `tests/test_undo.cc`) or create them if not present. Prefer
|
||||
using `Buffer` + `UndoSystem` directly for tight unit tests; add higher‑level integration tests as needed.
|
||||
|
||||
3) For C-k U uppercase mapping on macOS:
|
||||
- Add a temporary status dump when k-prefix suffix mapping falls back to TEXTINPUT path (we added stderr prints; also set Editor status with a short code like "K-TI U" during one session) to confirm path is used and suppression is working.
|
||||
- If TEXTINPUT never arrives, force suppression: when we detect k-prefix and KEYDOWN of a letter with Shift, preemptively handle via KEYDOWN-derived uppercase ASCII rather than deferring.
|
||||
6. Documentation updates — In progress
|
||||
- In `docs/undo.md` and `docs/undo-roadmap.md`:
|
||||
- Describe how to enable instrumentation (`KTE_UNDO_DEBUG`) and an example of trace logs.
|
||||
- List batching rules and commit boundaries clearly with examples.
|
||||
- Document current UTF‑8 semantics (byte‑wise vs codepoint‑wise) and any known limitations.
|
||||
- Current status: this `undo-state.md` updated; instrumentation how‑to and example traces pending.
|
||||
|
||||
4) Consolidate k-prefix handling:
|
||||
- After mapping a k-prefix suffix to a command (Undo/Redo/etc.), always set suppress_text_input_once_ = true to avoid any trailing TEXTINPUT.
|
||||
- Clear k_prefix_ reliably on both KEYDOWN and TEXTINPUT paths.
|
||||
7. CI and build hygiene — Pending
|
||||
- Default builds: `KTE_UNDO_DEBUG` OFF.
|
||||
- Add a CI job that builds and runs tests with `KTE_UNDO_DEBUG=ON` to ensure the instrumentation path remains
|
||||
healthy.
|
||||
- Ensure no performance regressions or excessive logging in release builds.
|
||||
|
||||
5) Once mapping is solid, remove all diagnostics and keep the minimal, deterministic logic.
|
||||
8. Stretch goals (optional, time‑boxed) — Pending
|
||||
- IME composition: confirm that `SDL_TEXTINPUT` behavior during IME composition does not produce partial/broken
|
||||
insertions; if needed, buffer composition updates into a single commit.
|
||||
- Ensure paste operations (multi‑line/UTF‑8) remain atomic in undo history.
|
||||
|
||||
Open questions for future debugging
|
||||
- Does SDL on this macOS setup deliver Shift+U as KEYDOWN+TEXTINPUT consistently, or only TEXTINPUT? We need a small on-screen debug to avoid relying on stderr.
|
||||
- Are there any IME/TextInput SDL hints on macOS we should set for raw key handling during k-prefix?
|
||||
- Should we temporarily disable SDL text input (SDL_StopTextInput) during k-prefix suffix processing to eliminate TEXTINPUT races on macOS?
|
||||
### How to run the tests
|
||||
|
||||
Notes on UndoSystem correctness (unrelated to the GUI mapping bug)
|
||||
- Undo tree invariants are implemented: pending is detached; commit attaches and clears redo branches; undo/redo apply low-level Buffer edits with no public editor paths; saved marker updated via mark_saved().
|
||||
- Dirty flag mirrors (current != saved).
|
||||
- Delete batching supports prepend for backspace sequences (stored text is in increasing column order from anchor).
|
||||
- Newline joins/splits are recorded as UndoType::Newline and committed immediately for single-step undo of line joins.
|
||||
- Configure with `-DBUILD_TESTS=ON` and build the `test_undo` target. Run the produced binary (e.g., `./test_undo`).
|
||||
The test prints progress and uses assertions to validate behavior.
|
||||
|
||||
Owner pointers & file locations
|
||||
- GUI mapping and suppression: GUIInputHandler.cc
|
||||
- Command layer commit boundaries: Command.cc (cmd_kprefix, cmd_undo, cmd_redo)
|
||||
- Undo batching entry points: Command.cc (cmd_insert_text, cmd_backspace, cmd_delete_char, cmd_newline)
|
||||
### Deliverables
|
||||
|
||||
End of snapshot — safe to resume from here.
|
||||
- CMake toggle for instrumentation and verified logs for core scenarios. (Pending)
|
||||
- Updated `GUIInputHandler.cc` solidifying `KEYDOWN` vs `TEXTINPUT` separation and suppression rules. (Completed)
|
||||
- Verified commit boundaries in `Command.cc` with comments where appropriate. (In progress)
|
||||
- New tests for branching, UTF‑8, and multi‑line operations; all passing. (Completed for listed scenarios)
|
||||
- Docs updated with how‑to and example traces. (Pending)
|
||||
|
||||
---
|
||||
### Acceptance criteria
|
||||
|
||||
RESOLUTION (2025-11-30)
|
||||
### Current status (2025‑12‑01) vs acceptance criteria
|
||||
|
||||
Root Cause Identified and Fixed
|
||||
The undo system failure was caused by incorrect timing of UndoSystem::Begin() and Append() calls relative to buffer modifications in Command.cc.
|
||||
|
||||
Problem:
|
||||
- In cmd_insert_text, cmd_backspace, cmd_delete_char, and cmd_newline, the undo recording (Begin/Append) was called BEFORE the actual buffer modification and cursor update.
|
||||
- UndoSystem::Begin() checks the current cursor position to determine if it can batch with the pending node.
|
||||
- For Insert type: Begin() checks if col == pending->col + pending->text.size()
|
||||
- For Delete type: Begin() checks if the cursor is at the expected position based on whether it's forward delete or backspace
|
||||
- When Begin/Append were called before cursor updates, the batching condition would fail on the second character because the cursor hadn't moved yet from the first insertion.
|
||||
- This caused each character to create a separate batch, but since commit() was never called between characters (only at k-prefix or undo), the pending node would be overwritten rather than committed, resulting in no undo history.
|
||||
|
||||
Fix Applied:
|
||||
- cmd_insert_text: Moved Begin/Append to AFTER buffer insertion (lines 854-856) and cursor update (line 857).
|
||||
- cmd_backspace: Moved Begin/Append to AFTER character deletion (lines 1024-1025) and cursor decrement (line 1026).
|
||||
- cmd_delete_char: Moved Begin/Append to AFTER character deletion (lines 1074-1076).
|
||||
- cmd_newline: Moved Begin/commit to AFTER line split (lines 956-966) and cursor update (lines 963-967).
|
||||
|
||||
Result:
|
||||
- Begin() now sees the correct cursor position after each edit, allowing proper batching of consecutive characters.
|
||||
- Typing "Hello" will now create a single pending batch with all 5 characters that can be undone as one unit.
|
||||
- The fix applies to both terminal (kte) and GUI (kge) builds.
|
||||
|
||||
Testing Recommendation:
|
||||
- Type several characters (e.g., "Hello")
|
||||
- Press C-k u to undo - the entire word should disappear
|
||||
- Press C-k U to redo - the word should reappear
|
||||
- Test backspace batching: type several characters, then backspace multiple times, then undo - should undo the backspace batch
|
||||
- Test delete batching similarly
|
||||
- Short instrumentation traces match expected batching and commit behavior for typing, backspace/delete, newline, and
|
||||
undo/redo. — Pending (instrumentation toggle + capture not done)
|
||||
- Printable input comes exclusively from `SDL_TEXTINPUT`; no stray inserts after meta/prefix/universal‑argument flows.
|
||||
— Satisfied (GUI path updated; terminal path consistent)
|
||||
- Undo branching behaves correctly; redo is discarded upon new commits after undo. — Satisfied (tested)
|
||||
- UTF‑8 and multi‑line scenarios round‑trip via undo/redo according to the documented semantics. — Satisfied (tested)
|
||||
- Tests pass with `KTE_UNDO_DEBUG` both OFF and ON. — Pending (no CMake toggle yet; default OFF passes)
|
||||
@@ -1,55 +0,0 @@
|
||||
# flake.nix
|
||||
{
|
||||
description = "kte ImGui/SDL2 text editor";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
packages.default = pkgs.stdenv.mkDerivation {
|
||||
pname = "kte";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
|
||||
buildInputs = with pkgs; [
|
||||
ncurses
|
||||
SDL2
|
||||
libGL
|
||||
xorg.libX11
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DBUILD_GUI=ON"
|
||||
"-DCURSES_NEED_NCURSES=TRUE"
|
||||
"-DCURSES_NEED_WIDE=TRUE"
|
||||
];
|
||||
|
||||
# Alternative (even stronger): completely hide the broken module
|
||||
preConfigure = ''
|
||||
# If the project ships its own FindSDL2.cmake in cmake/, hide it
|
||||
if [ -f cmake/FindSDL2.cmake ]; then
|
||||
mv cmake/FindSDL2.cmake cmake/FindSDL2.cmake.disabled
|
||||
echo "Disabled bundled FindSDL2.cmake"
|
||||
fi
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "kte ImGui/SDL2 GUI editor";
|
||||
mainProgram = "kte";
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ self.packages.${system}.default ];
|
||||
packages = with pkgs; [ gdb clang-tools ];
|
||||
};
|
||||
});
|
||||
}
|
||||
10
flake.lock
generated
10
flake.lock
generated
@@ -2,15 +2,15 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764242076,
|
||||
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1764517877,
|
||||
"narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
|
||||
"rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
|
||||
23
flake.nix
23
flake.nix
@@ -1,21 +1,20 @@
|
||||
{
|
||||
description = "Kyle's Text Editor";
|
||||
description = "kyle's text editor";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
};
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
inputs@{ self, nixpkgs, ... }:
|
||||
let
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; };
|
||||
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||
pkgsFor = system: import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
packages.x86_64-linux = {
|
||||
default = pkgs.callPackage ./default-nogui.nix { };
|
||||
kge = pkgs.callPackage ./default-gui.nix { };
|
||||
kte = pkgs.callPackage ./default-nogui.nix { };
|
||||
full = pkgs.callPackage ./default.nix { };
|
||||
};
|
||||
packages = eachSystem (system: rec {
|
||||
default = kte;
|
||||
full = kge;
|
||||
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
8
kte-cloc
8
kte-cloc
@@ -1,3 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc -fmt 3
|
||||
fmt_arg=""
|
||||
if [ "${V}" = "1" ]
|
||||
then
|
||||
fmt_args="-fmt 3"
|
||||
fi
|
||||
|
||||
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}
|
||||
|
||||
279
syntax/CppHighlighter.cc
Normal file
279
syntax/CppHighlighter.cc
Normal file
@@ -0,0 +1,279 @@
|
||||
#include "CppHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static bool
|
||||
is_digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
|
||||
CppHighlighter::CppHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
|
||||
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
|
||||
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
|
||||
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
|
||||
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
|
||||
};
|
||||
for (auto s: kw)
|
||||
keywords_.insert(s);
|
||||
const char *types[] = {
|
||||
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
|
||||
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t"
|
||||
};
|
||||
for (auto s: types)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CppHighlighter::is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CppHighlighter::is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
// Stateless entry simply delegates to stateful with a clean previous state
|
||||
StatefulHighlighter::LineState prev;
|
||||
(void) HighlightLineStateful(buf, row, prev, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
CppHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
if (s.empty())
|
||||
return state;
|
||||
|
||||
auto push = [&](int a, int b, TokenKind k) {
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
};
|
||||
int n = static_cast<int>(s.size());
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
int i = 0;
|
||||
|
||||
// Continue multi-line raw string from previous line
|
||||
if (state.in_raw_string) {
|
||||
std::string needle = ")" + state.raw_delim + "\"";
|
||||
auto pos = s.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
push(0, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(0, end, TokenKind::String);
|
||||
i = end;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Continue multi-line block comment from previous line
|
||||
if (state.in_block_comment) {
|
||||
int j = i;
|
||||
while (i + 1 < n) {
|
||||
if (s[i] == '*' && s[i + 1] == '/') {
|
||||
i += 2;
|
||||
push(j, i, TokenKind::Comment);
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (state.in_block_comment) {
|
||||
push(j, n, TokenKind::Comment);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
// Preprocessor at beginning of line (after leading whitespace)
|
||||
if (i == bol && c == '#') {
|
||||
push(0, n, TokenKind::Preproc);
|
||||
break;
|
||||
}
|
||||
|
||||
// Whitespace
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line comment
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
|
||||
// Block comment
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (closed) {
|
||||
push(i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// Spill to next lines
|
||||
push(i, n, TokenKind::Comment);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Raw string start: very simple detection: R"delim(
|
||||
if (c == 'R' && i + 1 < n && s[i + 1] == '"') {
|
||||
int k = i + 2;
|
||||
std::string delim;
|
||||
while (k < n && s[k] != '(') {
|
||||
delim.push_back(s[k]);
|
||||
++k;
|
||||
}
|
||||
if (k < n && s[k] == '(') {
|
||||
int body_start = k + 1;
|
||||
std::string needle = ")" + delim + "\"";
|
||||
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
|
||||
if (pos == std::string::npos) {
|
||||
push(i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If malformed, just treat 'R' as identifier fallback
|
||||
}
|
||||
|
||||
// Regular string literal
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Char literal
|
||||
if (c == '\'') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '\'')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::Char);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Number literal (simple)
|
||||
if (is_digit(c) || (c == '.' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||
s[j] == 'X' || s[j] == 'b' || s[j] == 'B' || s[j] == '_'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Identifier / keyword / type
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (keywords_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Operators and punctuation (single char for now)
|
||||
TokenKind kind = TokenKind::Operator;
|
||||
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
kind = TokenKind::Punctuation;
|
||||
push(i, i + 1, kind);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
push(i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
35
syntax/CppHighlighter.h
Normal file
35
syntax/CppHighlighter.h
Normal file
@@ -0,0 +1,35 @@
|
||||
// CppHighlighter.h - minimal stateless C/C++ line highlighter
|
||||
#pragma once
|
||||
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class CppHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
CppHighlighter();
|
||||
|
||||
~CppHighlighter() override = default;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> keywords_;
|
||||
std::unordered_set<std::string> types_;
|
||||
|
||||
static bool is_ident_start(char c);
|
||||
|
||||
static bool is_ident_char(char c);
|
||||
};
|
||||
} // namespace kte
|
||||
159
syntax/ErlangHighlighter.cc
Normal file
159
syntax/ErlangHighlighter.cc
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "ErlangHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_' || c == '\'';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '@' || c == ':' || c == '?';
|
||||
}
|
||||
|
||||
|
||||
ErlangHighlighter::ErlangHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"after", "begin", "case", "catch", "cond", "div", "end", "fun", "if", "let", "of",
|
||||
"receive", "when", "try", "rem", "and", "andalso", "orelse", "not", "band", "bor", "bxor",
|
||||
"bnot", "xor", "module", "export", "import", "record", "define", "undef", "include", "include_lib"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErlangHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// comment
|
||||
if (c == '%') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// strings
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// char literal $X
|
||||
if (c == '$') {
|
||||
int j = i + 1;
|
||||
if (j < n && s[j] == '\\' && j + 1 < n)
|
||||
j += 2;
|
||||
else if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Char);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// numbers
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '#' || s[j] == '.' ||
|
||||
s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// atoms/variables/identifiers (including quoted atoms)
|
||||
if (is_ident_start(c)) {
|
||||
// quoted atom: '...'
|
||||
if (c == '\'') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (d == '\'') {
|
||||
if (j < n && s[j] == '\'') {
|
||||
++j;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (d == '\\')
|
||||
esc = !esc;
|
||||
}
|
||||
push(out, i, j, TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
// lowercase leading -> atom/function/module; uppercase or '_' -> variable
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
// keyword check (lowercase)
|
||||
std::string lower;
|
||||
lower.reserve(id.size());
|
||||
for (char ch: id)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
if (kws_.count(lower))
|
||||
k = TokenKind::Keyword;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ',' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c ==
|
||||
'}')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
17
syntax/ErlangHighlighter.h
Normal file
17
syntax/ErlangHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// ErlangHighlighter.h - simple Erlang highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class ErlangHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
ErlangHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
121
syntax/ForthHighlighter.cc
Normal file
121
syntax/ForthHighlighter.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "ForthHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_word_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '>' || c == '<' || c == '?';
|
||||
}
|
||||
|
||||
|
||||
ForthHighlighter::ForthHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
":", ";", "if", "else", "then", "begin", "until", "while", "repeat",
|
||||
"do", "loop", "+loop", "leave", "again", "case", "of", "endof", "endcase",
|
||||
".", ".r", ".s", ".\"", ",", "cr", "emit", "type", "key",
|
||||
"+", "-", "*", "/", "mod", "/mod", "+-", "abs", "min", "max",
|
||||
"dup", "drop", "swap", "over", "rot", "-rot", "nip", "tuck", "pick", "roll",
|
||||
"and", "or", "xor", "invert", "lshift", "rshift",
|
||||
"variable", "constant", "value", "to", "create", "does>", "allot", ",",
|
||||
"cells", "cell+", "chars", "char+",
|
||||
"[", "]", "immediate",
|
||||
"s\"", ".\""
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ForthHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// backslash comment to end of line
|
||||
if (c == '\\') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// parenthesis comment ( ... ) if at word boundary
|
||||
if (c == '(') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != ')')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// strings: ." ... " and S" ... " and raw "..."
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != '"')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '#'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// word/identifier
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || std::ispunct(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_word_char(s[j]))
|
||||
++j;
|
||||
std::string w = s.substr(i, j - i);
|
||||
// normalize to lowercase for keyword compare (Forth is case-insensitive typically)
|
||||
std::string lower;
|
||||
lower.reserve(w.size());
|
||||
for (char ch: w)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
TokenKind k = kws_.count(lower) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
// Single-char punctuation fallback
|
||||
if (w.size() == 1 && std::ispunct(static_cast<unsigned char>(w[0])) && !kws_.count(lower)) {
|
||||
k = (w[0] == '(' || w[0] == ')' || w[0] == ',')
|
||||
? TokenKind::Punctuation
|
||||
: TokenKind::Operator;
|
||||
}
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
17
syntax/ForthHighlighter.h
Normal file
17
syntax/ForthHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// ForthHighlighter.h - simple Forth highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class ForthHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
ForthHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
157
syntax/GoHighlighter.cc
Normal file
157
syntax/GoHighlighter.cc
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "GoHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
GoHighlighter::GoHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
|
||||
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
|
||||
"switch", "type", "var"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
|
||||
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
// line comment
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"' || c == '`') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
if (q == '`') {
|
||||
while (j < n && s[j] != '`')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
} else {
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||
s[j] == 'X' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/GoHighlighter.h
Normal file
18
syntax/GoHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// GoHighlighter.h - simple Go highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class GoHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
GoHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
209
syntax/HighlighterEngine.cc
Normal file
209
syntax/HighlighterEngine.cc
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "HighlighterEngine.h"
|
||||
#include "../Buffer.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <thread>
|
||||
|
||||
namespace kte {
|
||||
HighlighterEngine::HighlighterEngine() = default;
|
||||
|
||||
|
||||
HighlighterEngine::~HighlighterEngine()
|
||||
{
|
||||
// stop background worker
|
||||
if (worker_running_.load()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
worker_running_.store(false);
|
||||
has_request_ = true; // wake it up to exit
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
hl_ = std::move(hl);
|
||||
cache_.clear();
|
||||
state_cache_.clear();
|
||||
state_last_contig_.clear();
|
||||
}
|
||||
|
||||
|
||||
const LineHighlight &
|
||||
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
auto it = cache_.find(row);
|
||||
if (it != cache_.end() && it->second.version == buf_version) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||
LineHighlight &slot = cache_[row];
|
||||
slot.version = buf_version;
|
||||
slot.spans.clear();
|
||||
|
||||
if (!hl_) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||
LanguageHighlighter *hl_ptr = hl_.get();
|
||||
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||
|
||||
if (!is_stateful) {
|
||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||
auto &out = slot.spans;
|
||||
lock.unlock();
|
||||
hl_ptr->HighlightLine(buf, row, out);
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||
// but release during heavy computation.
|
||||
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||
|
||||
StatefulHighlighter::LineState prev_state;
|
||||
int start_row = -1;
|
||||
if (!state_cache_.empty()) {
|
||||
// linear search over map (unordered), track best candidate
|
||||
int best = -1;
|
||||
for (const auto &kv: state_cache_) {
|
||||
int r = kv.first;
|
||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||
if (r > best)
|
||||
best = r;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
start_row = best;
|
||||
prev_state = state_cache_.at(best).state;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||
lock.unlock();
|
||||
StatefulHighlighter::LineState cur_state = prev_state;
|
||||
for (int r = start_row + 1; r <= row; ++r) {
|
||||
std::vector<HighlightSpan> tmp;
|
||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||
// Update state cache for r
|
||||
std::lock_guard<std::mutex> gl(mtx_);
|
||||
StateEntry se;
|
||||
se.version = buf_version;
|
||||
se.state = next_state;
|
||||
state_cache_[r] = se;
|
||||
cur_state = next_state;
|
||||
}
|
||||
|
||||
// Return reference under lock to ensure slot's address stability in map
|
||||
lock.lock();
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::InvalidateFrom(int row)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (cache_.empty())
|
||||
return;
|
||||
// Simple implementation: erase all rows >= row
|
||||
for (auto it = cache_.begin(); it != cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
if (!state_cache_.empty()) {
|
||||
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = state_cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::ensure_worker_started() const
|
||||
{
|
||||
if (worker_running_.load())
|
||||
return;
|
||||
worker_running_.store(true);
|
||||
worker_ = std::thread([this]() {
|
||||
this->worker_loop();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::worker_loop() const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
while (worker_running_.load()) {
|
||||
cv_.wait(lock, [this]() {
|
||||
return has_request_ || !worker_running_.load();
|
||||
});
|
||||
if (!worker_running_.load())
|
||||
break;
|
||||
WarmRequest req = pending_;
|
||||
has_request_ = false;
|
||||
// Copy locals then release lock while computing
|
||||
lock.unlock();
|
||||
if (req.buf) {
|
||||
int start = std::max(0, req.start_row);
|
||||
int end = std::max(start, req.end_row);
|
||||
for (int r = start; r <= end; ++r) {
|
||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||
// Compute line; GetLine is thread-safe
|
||||
(void) this->GetLine(*req.buf, r, req.version);
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin) const
|
||||
{
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
// Synchronously compute visible rows to ensure cache hits during draw
|
||||
int start = std::max(0, first_row);
|
||||
int end = start + row_count - 1;
|
||||
int max_rows = static_cast<int>(buf.Nrows());
|
||||
if (start >= max_rows)
|
||||
return;
|
||||
if (end >= max_rows)
|
||||
end = max_rows - 1;
|
||||
|
||||
for (int r = start; r <= end; ++r) {
|
||||
(void) GetLine(buf, r, buf_version);
|
||||
}
|
||||
|
||||
// Enqueue background warm-around
|
||||
int warm_start = std::max(0, start - warm_margin);
|
||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
pending_.buf = &buf;
|
||||
pending_.version = buf_version;
|
||||
pending_.start_row = warm_start;
|
||||
pending_.end_row = warm_end;
|
||||
has_request_ = true;
|
||||
}
|
||||
ensure_worker_started();
|
||||
cv_.notify_one();
|
||||
}
|
||||
} // namespace kte
|
||||
85
syntax/HighlighterEngine.h
Normal file
85
syntax/HighlighterEngine.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// HighlighterEngine.h - caching layer for per-line highlights
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "../Highlight.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class HighlighterEngine {
|
||||
public:
|
||||
HighlighterEngine();
|
||||
|
||||
~HighlighterEngine();
|
||||
|
||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||
|
||||
// Retrieve highlights for a given line and buffer version.
|
||||
// If cache is stale, recompute using the current highlighter.
|
||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||
|
||||
// Invalidate cached lines from row (inclusive)
|
||||
void InvalidateFrom(int row);
|
||||
|
||||
|
||||
bool HasHighlighter() const
|
||||
{
|
||||
return static_cast<bool>(hl_);
|
||||
}
|
||||
|
||||
|
||||
// Phase 3: viewport-first prefetch and background warming
|
||||
// Compute only the visible range now, and enqueue a background warm-around task.
|
||||
// warm_margin: how many extra lines above/below to warm in the background.
|
||||
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin = 200) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<LanguageHighlighter> hl_;
|
||||
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||
|
||||
// For stateful highlighters, remember per-line state (state after finishing that row)
|
||||
struct StateEntry {
|
||||
std::uint64_t version{0};
|
||||
// Using the interface type; forward-declare via header
|
||||
StatefulHighlighter::LineState state;
|
||||
};
|
||||
|
||||
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||
|
||||
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||
|
||||
// Thread-safety for caches and background worker state
|
||||
mutable std::mutex mtx_;
|
||||
|
||||
// Background warmer
|
||||
struct WarmRequest {
|
||||
const Buffer *buf{nullptr};
|
||||
std::uint64_t version{0};
|
||||
int start_row{0};
|
||||
int end_row{0}; // inclusive
|
||||
};
|
||||
|
||||
mutable std::condition_variable cv_;
|
||||
mutable std::thread worker_;
|
||||
mutable std::atomic<bool> worker_running_{false};
|
||||
mutable bool has_request_{false};
|
||||
mutable WarmRequest pending_{};
|
||||
|
||||
void ensure_worker_started() const;
|
||||
|
||||
void worker_loop() const;
|
||||
};
|
||||
} // namespace kte
|
||||
247
syntax/HighlighterRegistry.cc
Normal file
247
syntax/HighlighterRegistry.cc
Normal file
@@ -0,0 +1,247 @@
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "CppHighlighter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
|
||||
// Forward declare simple highlighters implemented in this project
|
||||
namespace kte {
|
||||
// Registration storage
|
||||
struct RegEntry {
|
||||
std::string ft; // normalized
|
||||
HighlighterRegistry::Factory factory;
|
||||
};
|
||||
|
||||
|
||||
static std::vector<RegEntry> &
|
||||
registry()
|
||||
{
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
|
||||
class JSONHighlighter;
|
||||
class MarkdownHighlighter;
|
||||
class ShellHighlighter;
|
||||
class GoHighlighter;
|
||||
class PythonHighlighter;
|
||||
class RustHighlighter;
|
||||
class LispHighlighter;
|
||||
class SqlHighlighter;
|
||||
class ErlangHighlighter;
|
||||
class ForthHighlighter;
|
||||
}
|
||||
|
||||
// Headers for the above
|
||||
#include "JsonHighlighter.h"
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "ShellHighlighter.h"
|
||||
#include "GoHighlighter.h"
|
||||
#include "PythonHighlighter.h"
|
||||
#include "RustHighlighter.h"
|
||||
#include "LispHighlighter.h"
|
||||
#include "SqlHighlighter.h"
|
||||
#include "ErlangHighlighter.h"
|
||||
#include "ForthHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
static std::string
|
||||
to_lower(std::string_view s)
|
||||
{
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::Normalize(std::string_view ft)
|
||||
{
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
|
||||
return "cpp";
|
||||
if (f == "cpp")
|
||||
return "cpp";
|
||||
if (f == "json")
|
||||
return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
|
||||
return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
|
||||
return "shell";
|
||||
if (f == "go" || f == "golang")
|
||||
return "go";
|
||||
if (f == "py" || f == "python")
|
||||
return "python";
|
||||
if (f == "rs" || f == "rust")
|
||||
return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
|
||||
"cl")
|
||||
return "lisp";
|
||||
if (f == "sql" || f == "sqlite" || f == "sqlite3")
|
||||
return "sql";
|
||||
if (f == "erlang" || f == "erl" || f == "hrl")
|
||||
return "erlang";
|
||||
if (f == "forth" || f == "fth" || f == "4th" || f == "fs")
|
||||
return "forth";
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<LanguageHighlighter>
|
||||
HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
// Prefer externally registered factories
|
||||
for (const auto &e: registry()) {
|
||||
if (e.ft == ft && e.factory)
|
||||
return e.factory();
|
||||
}
|
||||
if (ft == "cpp")
|
||||
return std::make_unique<CppHighlighter>();
|
||||
if (ft == "json")
|
||||
return std::make_unique<JSONHighlighter>();
|
||||
if (ft == "markdown")
|
||||
return std::make_unique<MarkdownHighlighter>();
|
||||
if (ft == "shell")
|
||||
return std::make_unique<ShellHighlighter>();
|
||||
if (ft == "go")
|
||||
return std::make_unique<GoHighlighter>();
|
||||
if (ft == "python")
|
||||
return std::make_unique<PythonHighlighter>();
|
||||
if (ft == "rust")
|
||||
return std::make_unique<RustHighlighter>();
|
||||
if (ft == "lisp")
|
||||
return std::make_unique<LispHighlighter>();
|
||||
if (ft == "sql")
|
||||
return std::make_unique<SqlHighlighter>();
|
||||
if (ft == "erlang")
|
||||
return std::make_unique<ErlangHighlighter>();
|
||||
if (ft == "forth")
|
||||
return std::make_unique<ForthHighlighter>();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
static std::string
|
||||
shebang_to_ft(std::string_view first_line)
|
||||
{
|
||||
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
|
||||
return "";
|
||||
std::string low = to_lower(first_line);
|
||||
if (low.find("python") != std::string::npos)
|
||||
return "python";
|
||||
if (low.find("bash") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("sh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("zsh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("fish") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
|
||||
std::string::npos)
|
||||
return "lisp";
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||
{
|
||||
// Extension
|
||||
std::string p(path);
|
||||
std::error_code ec;
|
||||
std::string ext = std::filesystem::path(p).extension().string();
|
||||
for (auto &ch: ext)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (!ext.empty()) {
|
||||
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
|
||||
== ".hh")
|
||||
return "cpp";
|
||||
if (ext == ".json")
|
||||
return "json";
|
||||
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
|
||||
return "markdown";
|
||||
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
|
||||
return "shell";
|
||||
if (ext == ".go")
|
||||
return "go";
|
||||
if (ext == ".py")
|
||||
return "python";
|
||||
if (ext == ".rs")
|
||||
return "rust";
|
||||
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|
||||
|| ext == ".cl")
|
||||
return "lisp";
|
||||
if (ext == ".sql" || ext == ".sqlite")
|
||||
return "sql";
|
||||
if (ext == ".erl" || ext == ".hrl")
|
||||
return "erlang";
|
||||
if (ext == ".forth" || ext == ".fth" || ext == ".4th" || ext == ".fs")
|
||||
return "forth";
|
||||
}
|
||||
// Shebang
|
||||
std::string ft = shebang_to_ft(first_line);
|
||||
return ft;
|
||||
}
|
||||
} // namespace kte
|
||||
|
||||
// Extensibility API implementations
|
||||
namespace kte {
|
||||
void
|
||||
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (auto &e: registry()) {
|
||||
if (e.ft == ft) {
|
||||
if (override_existing)
|
||||
e.factory = std::move(factory);
|
||||
return;
|
||||
}
|
||||
}
|
||||
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (const auto &e: registry())
|
||||
if (e.ft == ft)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
HighlighterRegistry::RegisteredFiletypes()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(registry().size());
|
||||
for (const auto &e: registry())
|
||||
out.push_back(e.ft);
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declare adapter factory
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
|
||||
void
|
||||
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)())
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
Register(ft, [ft, get_language]() {
|
||||
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||
}, /*override_existing=*/true);
|
||||
}
|
||||
#endif
|
||||
} // namespace kte
|
||||
47
syntax/HighlighterRegistry.h
Normal file
47
syntax/HighlighterRegistry.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class HighlighterRegistry {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
|
||||
|
||||
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
|
||||
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||
|
||||
// Detect filetype by path extension and shebang (first line).
|
||||
// Returns normalized id or empty string if unknown.
|
||||
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||
|
||||
// Normalize various aliases/extensions to canonical ids.
|
||||
static std::string Normalize(std::string_view ft);
|
||||
|
||||
// Extensibility: allow external code to register highlighters at runtime.
|
||||
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||
// normalized key and override=false, the existing factory is kept.
|
||||
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||
|
||||
// Returns true if a factory is registered for the (normalized) filetype.
|
||||
static bool IsRegistered(std::string_view filetype);
|
||||
|
||||
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||
static std::vector<std::string> RegisteredFiletypes();
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declaration to avoid hard dependency when disabled.
|
||||
struct TSLanguage;
|
||||
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||
// The getter should return a non-null language pointer for the grammar.
|
||||
static void RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)());
|
||||
#endif
|
||||
};
|
||||
} // namespace kte
|
||||
90
syntax/JsonHighlighter.cc
Normal file
90
syntax/JsonHighlighter.cc
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "JsonHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static bool
|
||||
is_digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
auto push = [&](int a, int b, TokenKind k) {
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_digit(c) || (c == '-' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'e' ||
|
||||
s[j] == 'E' || s[j] == '+' || s[j] == '-' || s[j] == '_'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// booleans/null
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && std::isalpha(static_cast<unsigned char>(s[j])))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
if (id == "true" || id == "false" || id == "null")
|
||||
push(i, j, TokenKind::Constant);
|
||||
else
|
||||
push(i, j, TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// punctuation
|
||||
if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
|
||||
push(i, i + 1, TokenKind::Punctuation);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
// fallback
|
||||
push(i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
12
syntax/JsonHighlighter.h
Normal file
12
syntax/JsonHighlighter.h
Normal file
@@ -0,0 +1,12 @@
|
||||
// JsonHighlighter.h - simple JSON line highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <vector>
|
||||
|
||||
namespace kte {
|
||||
class JSONHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
} // namespace kte
|
||||
51
syntax/LanguageHighlighter.h
Normal file
51
syntax/LanguageHighlighter.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// LanguageHighlighter.h - interface for line-based highlighters
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "../Highlight.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class LanguageHighlighter {
|
||||
public:
|
||||
virtual ~LanguageHighlighter() = default;
|
||||
|
||||
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
|
||||
virtual bool Stateful() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
|
||||
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
|
||||
class StatefulHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
struct LineState {
|
||||
bool in_block_comment{false};
|
||||
bool in_raw_string{false};
|
||||
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||
std::string raw_delim;
|
||||
};
|
||||
|
||||
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||
// Implementations should append spans for this line to out and compute the next state.
|
||||
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
|
||||
bool Stateful() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace kte
|
||||
107
syntax/LispHighlighter.cc
Normal file
107
syntax/LispHighlighter.cc
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "LispHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
LispHighlighter::LispHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
|
||||
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol < n && s[bol] == ';') {
|
||||
push(out, bol, n, TokenKind::Comment);
|
||||
if (bol > 0)
|
||||
push(out, 0, bol, TokenKind::Whitespace);
|
||||
return;
|
||||
}
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
|
||||
'_') {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
|
||||
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
17
syntax/LispHighlighter.h
Normal file
17
syntax/LispHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// LispHighlighter.h - simple Lisp/Scheme family highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class LispHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
LispHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
132
syntax/MarkdownHighlighter.cc
Normal file
132
syntax/MarkdownHighlighter.cc
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st; // not used in stateless entry
|
||||
(void) HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Reuse in_block_comment flag as "in fenced code" state.
|
||||
if (state.in_block_comment) {
|
||||
// If line contains closing fence ``` then close after it
|
||||
auto pos = s.find("```");
|
||||
if (pos == std::string::npos) {
|
||||
push_span(out, 0, n, TokenKind::String);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push_span(out, 0, end, TokenKind::String);
|
||||
// rest of line processed normally after fence
|
||||
int i = end;
|
||||
// whitespace
|
||||
if (i < n)
|
||||
push_span(out, i, n, TokenKind::Default);
|
||||
state.in_block_comment = false;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect fenced code block start at beginning (allow leading spaces)
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
|
||||
push_span(out, bol, n, TokenKind::String);
|
||||
state.in_block_comment = true; // enter fenced mode
|
||||
return state;
|
||||
}
|
||||
|
||||
// Headings: lines starting with 1-6 '#'
|
||||
if (bol < n && s[bol] == '#') {
|
||||
int j = bol;
|
||||
while (j < n && s[j] == '#')
|
||||
++j; // hashes
|
||||
// include following space and text as Keyword to stand out
|
||||
push_span(out, bol, n, TokenKind::Keyword);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Process inline: emphasis and code spans
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == '`') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != '`')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '*' || c == '_') {
|
||||
// bold/italic markers: treat the marker and until next same marker as Type to highlight
|
||||
char m = c;
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != m)
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::Type);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// links []() minimal: treat [text](url) as Function
|
||||
if (c == '[') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != ']')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j; // include ]
|
||||
if (j < n && s[j] == '(') {
|
||||
while (j < n && s[j] != ')')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
}
|
||||
push_span(out, i, j, TokenKind::Function);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// whitespace
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// fallback: default single char
|
||||
push_span(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
14
syntax/MarkdownHighlighter.h
Normal file
14
syntax/MarkdownHighlighter.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// MarkdownHighlighter.h - simple Markdown highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class MarkdownHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
} // namespace kte
|
||||
17
syntax/NullHighlighter.cc
Normal file
17
syntax/NullHighlighter.cc
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "NullHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
|
||||
namespace kte {
|
||||
void
|
||||
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
if (n <= 0)
|
||||
return;
|
||||
out.push_back({0, n, TokenKind::Default});
|
||||
}
|
||||
} // namespace kte
|
||||
11
syntax/NullHighlighter.h
Normal file
11
syntax/NullHighlighter.h
Normal file
@@ -0,0 +1,11 @@
|
||||
// NullHighlighter.h - default highlighter that emits a single Default span per line
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class NullHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
} // namespace kte
|
||||
172
syntax/PythonHighlighter.cc
Normal file
172
syntax/PythonHighlighter.cc
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "PythonHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
PythonHighlighter::PythonHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
|
||||
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
|
||||
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st;
|
||||
(void) HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||
auto pos = s.find(state.raw_delim);
|
||||
if (pos == std::string::npos) {
|
||||
push(out, 0, n, TokenKind::String);
|
||||
return state; // still inside
|
||||
} else {
|
||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||
push(out, 0, end, TokenKind::String);
|
||||
// remainder processed normally
|
||||
s = s.substr(end);
|
||||
n = static_cast<int>(s.size());
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
// Continue parsing remainder as a separate small loop
|
||||
int base = end;
|
||||
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||
// For simplicity, mark rest as Default
|
||||
if (n > 0)
|
||||
push(out, base, base + n, TokenKind::Default);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
// Detect comment start '#', ignoring inside strings
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '#') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// Strings: triple quotes and single-line
|
||||
if (c == '"' || c == '\'') {
|
||||
char q = c;
|
||||
// triple?
|
||||
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
|
||||
std::string delim(3, q);
|
||||
int j = i + 3; // search for closing triple
|
||||
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||
if (pos == std::string::npos) {
|
||||
push(out, i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push(out, i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == q)
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
20
syntax/PythonHighlighter.h
Normal file
20
syntax/PythonHighlighter.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// PythonHighlighter.h - simple Python highlighter with triple-quote state
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class PythonHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
PythonHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
145
syntax/RustHighlighter.cc
Normal file
145
syntax/RustHighlighter.cc
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "RustHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
RustHighlighter::RustHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
|
||||
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
|
||||
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
|
||||
"await", "try"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
|
||||
"bool", "char", "str"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/RustHighlighter.h
Normal file
18
syntax/RustHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// RustHighlighter.h - simple Rust highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class RustHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
RustHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
105
syntax/ShellHighlighter.cc
Normal file
105
syntax/ShellHighlighter.cc
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "ShellHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
// if first non-space is '#', whole line is comment
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol < n && s[bol] == '#') {
|
||||
push(out, bol, n, TokenKind::Comment);
|
||||
if (bol > 0)
|
||||
push(out, 0, bol, TokenKind::Whitespace);
|
||||
return;
|
||||
}
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '#') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '\'' || c == '"') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (q == '"') {
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
} else {
|
||||
if (d == '\'')
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// simple keywords
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '_'))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
static const char *kws[] = {
|
||||
"if", "then", "fi", "for", "in", "do", "done", "case", "esac", "while", "function",
|
||||
"elif", "else"
|
||||
};
|
||||
bool kw = false;
|
||||
for (auto k: kws)
|
||||
if (id == k) {
|
||||
kw = true;
|
||||
break;
|
||||
}
|
||||
push(out, i, j, kw ? TokenKind::Keyword : TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == '(' || c == ')' || c == '{' || c == '}' || c == ',' || c == ';')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
11
syntax/ShellHighlighter.h
Normal file
11
syntax/ShellHighlighter.h
Normal file
@@ -0,0 +1,11 @@
|
||||
// ShellHighlighter.h - simple POSIX shell highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class ShellHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
} // namespace kte
|
||||
156
syntax/SqlHighlighter.cc
Normal file
156
syntax/SqlHighlighter.cc
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "SqlHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
|
||||
}
|
||||
|
||||
|
||||
SqlHighlighter::SqlHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
|
||||
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
|
||||
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
|
||||
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
|
||||
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
|
||||
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
|
||||
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
|
||||
for (auto s: types)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// line comments: -- ...
|
||||
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// simple block comment on same line: /* ... */
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// strings: '...' or "..."
|
||||
if (c == '\'' || c == '"') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (d == q) {
|
||||
// Handle doubled quote escaping for SQL single quotes
|
||||
if (q == '\'' && j < n && s[j] == '\'') {
|
||||
++j;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = !esc;
|
||||
} else {
|
||||
esc = false;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
std::string lower;
|
||||
lower.reserve(id.size());
|
||||
for (char ch: id)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(lower))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(lower))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ',' || c == ';' || c == '(' || c == ')')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/SqlHighlighter.h
Normal file
18
syntax/SqlHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// SqlHighlighter.h - simple SQL/SQLite highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class SqlHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
SqlHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
51
syntax/TreeSitterHighlighter.cc
Normal file
51
syntax/TreeSitterHighlighter.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "TreeSitterHighlighter.h"
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
|
||||
#include "Buffer.h"
|
||||
#include <utility>
|
||||
|
||||
namespace kte {
|
||||
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
|
||||
: language_(lang), filetype_(std::move(filetype)) {}
|
||||
|
||||
|
||||
TreeSitterHighlighter::~TreeSitterHighlighter()
|
||||
{
|
||||
disposeParser();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
|
||||
{
|
||||
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
|
||||
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||
// and build tree_ from the buffer contents.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::disposeParser() const
|
||||
{
|
||||
// Stub; nothing to dispose when not actually creating parser/tree
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
|
||||
{
|
||||
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<LanguageHighlighter>
|
||||
CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)())
|
||||
{
|
||||
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
|
||||
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
|
||||
}
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
48
syntax/TreeSitterHighlighter.h
Normal file
48
syntax/TreeSitterHighlighter.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// TreeSitterHighlighter.h - optional adapter for Tree-sitter (behind KTE_ENABLE_TREESITTER)
|
||||
#pragma once
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
// Forward-declare Tree-sitter C API to avoid hard coupling in headers if includes are not present
|
||||
extern "C" {
|
||||
struct TSLanguage;
|
||||
struct TSParser;
|
||||
struct TSTree;
|
||||
}
|
||||
|
||||
namespace kte {
|
||||
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
|
||||
// does very limited token classification. This acts as a scaffold for future richer
|
||||
// queries. If no queries are provided, it currently produces no spans (safe fallback).
|
||||
class TreeSitterHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
|
||||
|
||||
~TreeSitterHighlighter() override;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
const TSLanguage *language_{nullptr};
|
||||
std::string filetype_;
|
||||
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||
mutable TSParser *parser_{nullptr};
|
||||
mutable TSTree *tree_{nullptr};
|
||||
|
||||
void ensureParsed(const Buffer &buf) const;
|
||||
|
||||
void disposeParser() const;
|
||||
};
|
||||
|
||||
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
235
test_undo.cc
235
test_undo.cc
@@ -94,6 +94,241 @@ main()
|
||||
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
||||
std::cout << " ✓ Redo successful - text restored\n\n";
|
||||
|
||||
// Test 4: Branching behavior – redo is discarded after new edits
|
||||
std::cout << "Test 4: Branching behavior (redo discarded after new edits)\n";
|
||||
// Reset to empty by undoing the last redo and the original insert, then reinsert 'abc'
|
||||
// Ensure buffer is empty before starting this scenario
|
||||
frontend.Input().QueueCommand(CommandId::Undo); // undo Hello
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "");
|
||||
|
||||
// Type a contiguous word 'abc' (single batch)
|
||||
frontend.Input().QueueText("abc");
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abc");
|
||||
|
||||
// Undo once – should remove the whole batch and leave empty
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "");
|
||||
|
||||
// Now type new text 'X' – this should create a new branch and discard old redo chain
|
||||
frontend.Input().QueueText("X");
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "X");
|
||||
|
||||
// Attempt Redo – should be a no-op (redo branch was discarded by new edit)
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "X");
|
||||
// Undo and Redo along the new branch should still work
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "X");
|
||||
std::cout << " ✓ Redo discarded after new edit; new branch undo/redo works\n\n";
|
||||
|
||||
// Clear buffer state for next tests: undo to empty if needed
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "");
|
||||
|
||||
// Test 5: UTF-8 insertion and undo/redo round-trip
|
||||
std::cout << "Test 5: UTF-8 insertion 'é漢' and undo/redo\n";
|
||||
const std::string utf8_text = "é漢"; // multi-byte UTF-8 (2 bytes + 3 bytes)
|
||||
frontend.Input().QueueText(utf8_text);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||
// Undo should remove the entire contiguous insertion batch
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "");
|
||||
// Redo restores it
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||
std::cout << " ✓ UTF-8 insert round-trips with undo/redo\n\n";
|
||||
|
||||
// Clear for next test
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "");
|
||||
|
||||
// Test 6: Multi-line operations (newline split and join via backspace at BOL)
|
||||
std::cout << "Test 6: Newline split and join via backspace at BOL\n";
|
||||
// Insert "ab" then newline then "cd" → expect two lines
|
||||
frontend.Input().QueueText("ab");
|
||||
frontend.Input().QueueCommand(CommandId::Newline);
|
||||
frontend.Input().QueueText("cd");
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(buf->Rows().size() >= 2);
|
||||
assert(std::string(buf->Rows()[0]) == "ab");
|
||||
assert(std::string(buf->Rows()[1]) == "cd");
|
||||
std::cout << " ✓ Split into two lines\n";
|
||||
|
||||
// Undo once – should remove "cd" insertion leaving two lines ["ab", ""] or join depending on commit
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
// Current design batches typing on the second line; after undo, the second line should exist but be empty
|
||||
assert(buf->Rows().size() >= 2);
|
||||
assert(std::string(buf->Rows()[0]) == "ab");
|
||||
assert(std::string(buf->Rows()[1]) == "");
|
||||
|
||||
// Undo the newline – should rejoin to a single line "ab"
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(buf->Rows().size() >= 1);
|
||||
assert(std::string(buf->Rows()[0]) == "ab");
|
||||
|
||||
// Redo twice to get back to ["ab","cd"]
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "ab");
|
||||
assert(std::string(buf->Rows()[1]) == "cd");
|
||||
std::cout << " ✓ Newline undo/redo round-trip\n";
|
||||
|
||||
// Now join via Backspace at beginning of second line
|
||||
frontend.Input().QueueCommand(CommandId::MoveDown); // ensure we're on the second line
|
||||
frontend.Input().QueueCommand(CommandId::MoveHome); // go to BOL on second line
|
||||
frontend.Input().QueueCommand(CommandId::Backspace); // join with previous line
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(buf->Rows().size() >= 1);
|
||||
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||
std::cout << " ✓ Backspace at BOL joins lines\n";
|
||||
|
||||
// Undo/Redo the join
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(buf->Rows().size() >= 1);
|
||||
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||
std::cout << " ✓ Join undo/redo round-trip\n\n";
|
||||
|
||||
// Test 7: Typing batching – a contiguous word undone in one step
|
||||
std::cout << "Test 7: Typing batching (single undo removes whole word)\n";
|
||||
// Clear current line first
|
||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]).empty());
|
||||
// Type a word and verify one undo clears it
|
||||
frontend.Input().QueueText("hello");
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "hello");
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]).empty());
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "hello");
|
||||
std::cout << " ✓ Contiguous typing batched into single undo step\n\n";
|
||||
|
||||
// Test 8: Forward delete batching at a fixed anchor column
|
||||
std::cout << "Test 8: Forward delete batching at fixed anchor (DeleteChar)\n";
|
||||
// Prepare line content
|
||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
frontend.Input().QueueText("abcdef");
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
// Ensure cursor at anchor column 0
|
||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||
// Delete three chars at cursor; should batch into one Delete node
|
||||
frontend.Input().QueueCommand(CommandId::DeleteChar, "", 3);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "def");
|
||||
// Single undo should restore the entire deleted run
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||
// Redo should remove the same run again
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "def");
|
||||
std::cout << " ✓ Forward delete batched and undo/redo round-trips\n\n";
|
||||
|
||||
// Test 9: Backspace batching with prepend rule (cursor moves left)
|
||||
std::cout << "Test 9: Backspace batching with prepend rule\n";
|
||||
// Restore to full string then backspace a run
|
||||
frontend.Input().QueueCommand(CommandId::Undo); // bring back to "abcdef"
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||
// Move to end and backspace three characters; should batch into one Delete node
|
||||
frontend.Input().QueueCommand(CommandId::MoveEnd);
|
||||
frontend.Input().QueueCommand(CommandId::Backspace, "", 3);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abc");
|
||||
// Single undo restores the deleted run
|
||||
frontend.Input().QueueCommand(CommandId::Undo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||
// Redo removes it again
|
||||
frontend.Input().QueueCommand(CommandId::Redo);
|
||||
while (!frontend.Input().IsEmpty() && running) {
|
||||
frontend.Step(editor, running);
|
||||
}
|
||||
assert(std::string(buf->Rows()[0]) == "abc");
|
||||
std::cout << " ✓ Backspace run batched and undo/redo round-trips\n\n";
|
||||
|
||||
frontend.Shutdown();
|
||||
|
||||
std::cout << "====================================\n";
|
||||
|
||||
177
themes/EInk.h
Normal file
177
themes/EInk.h
Normal file
@@ -0,0 +1,177 @@
|
||||
// themes/EInk.h — Monochrome e-ink inspired ImGui themes (header-only)
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyEInkImGuiTheme()
|
||||
{
|
||||
// E-Ink grayscale palette (light background)
|
||||
const ImVec4 paper = RGBA(0xF2F2EE); // light paper
|
||||
const ImVec4 bg1 = RGBA(0xE6E6E2);
|
||||
const ImVec4 bg2 = RGBA(0xDADAD5);
|
||||
const ImVec4 bg3 = RGBA(0xCFCFCA);
|
||||
const ImVec4 ink = RGBA(0x111111); // primary text (near black)
|
||||
const ImVec4 dim = RGBA(0x666666); // disabled text
|
||||
const ImVec4 border = RGBA(0xB8B8B3);
|
||||
const ImVec4 accent = RGBA(0x222222); // controls/active
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
colors[ImGuiCol_CheckMark] = accent;
|
||||
colors[ImGuiCol_SliderGrab] = accent;
|
||||
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||
colors[ImGuiCol_SeparatorActive] = accent;
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = accent;
|
||||
colors[ImGuiCol_NavHighlight] = accent;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = accent;
|
||||
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||
colors[ImGuiCol_PlotHistogram] = accent;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
ApplyEInkDarkImGuiTheme()
|
||||
{
|
||||
// E-Ink dark variant (dark background, light ink)
|
||||
const ImVec4 paper = RGBA(0x1A1A1A);
|
||||
const ImVec4 bg1 = RGBA(0x222222);
|
||||
const ImVec4 bg2 = RGBA(0x2B2B2B);
|
||||
const ImVec4 bg3 = RGBA(0x343434);
|
||||
const ImVec4 ink = RGBA(0xEDEDEA);
|
||||
const ImVec4 dim = RGBA(0xB5B5B3);
|
||||
const ImVec4 border = RGBA(0x444444);
|
||||
const ImVec4 accent = RGBA(0xDDDDDD);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
colors[ImGuiCol_CheckMark] = accent;
|
||||
colors[ImGuiCol_SliderGrab] = accent;
|
||||
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||
colors[ImGuiCol_SeparatorActive] = accent;
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = accent;
|
||||
colors[ImGuiCol_NavHighlight] = accent;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = accent;
|
||||
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||
colors[ImGuiCol_PlotHistogram] = accent;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||
}
|
||||
204
themes/Gruvbox.h
Normal file
204
themes/Gruvbox.h
Normal file
@@ -0,0 +1,204 @@
|
||||
// themes/Gruvbox.h — Gruvbox Dark/Light (medium) ImGui themes (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyGruvboxDarkMediumTheme()
|
||||
{
|
||||
// Gruvbox (dark, medium) palette
|
||||
const ImVec4 bg0 = RGBA(0x282828); // dark0
|
||||
const ImVec4 bg1 = RGBA(0x3C3836); // dark1
|
||||
const ImVec4 bg2 = RGBA(0x504945); // dark2
|
||||
const ImVec4 bg3 = RGBA(0x665C54); // dark3
|
||||
const ImVec4 fg1 = RGBA(0xEBDBB2); // light1
|
||||
const ImVec4 fg0 = RGBA(0xFBF1C7); // light0
|
||||
// accent colors
|
||||
const ImVec4 yellow = RGBA(0xFABD2F);
|
||||
const ImVec4 blue = RGBA(0x83A598);
|
||||
const ImVec4 aqua = RGBA(0x8EC07C);
|
||||
const ImVec4 orange = RGBA(0xFE8019);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = fg1;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = bg2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = aqua;
|
||||
colors[ImGuiCol_SliderGrab] = aqua;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
|
||||
colors[ImGuiCol_Separator] = bg2;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||
colors[ImGuiCol_SeparatorActive] = blue;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = orange;
|
||||
colors[ImGuiCol_NavHighlight] = orange;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = aqua;
|
||||
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
ApplyGruvboxLightMediumTheme()
|
||||
{
|
||||
// Gruvbox (light, medium) palette
|
||||
const ImVec4 bg0 = RGBA(0xFBF1C7); // light0
|
||||
const ImVec4 bg1 = RGBA(0xEBDBB2); // light1
|
||||
const ImVec4 bg2 = RGBA(0xD5C4A1); // light2
|
||||
const ImVec4 bg3 = RGBA(0xBDAE93); // light3
|
||||
const ImVec4 fg1 = RGBA(0x3C3836); // dark1
|
||||
const ImVec4 fg0 = RGBA(0x282828); // dark0
|
||||
// accents
|
||||
const ImVec4 yellow = RGBA(0xB57614);
|
||||
const ImVec4 blue = RGBA(0x076678);
|
||||
const ImVec4 aqua = RGBA(0x427B58);
|
||||
const ImVec4 orange = RGBA(0xAF3A03);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = fg1;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = bg2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = aqua;
|
||||
colors[ImGuiCol_SliderGrab] = aqua;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
|
||||
colors[ImGuiCol_Separator] = bg2;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||
colors[ImGuiCol_SeparatorActive] = blue;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = orange;
|
||||
colors[ImGuiCol_NavHighlight] = orange;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = aqua;
|
||||
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
111
themes/Nord.h
Normal file
111
themes/Nord.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// themes/Nord.h — Nord-inspired ImGui theme (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyNordImGuiTheme()
|
||||
{
|
||||
// Nord palette
|
||||
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
|
||||
const ImVec4 nord1 = RGBA(0x3B4252);
|
||||
const ImVec4 nord2 = RGBA(0x434C5E);
|
||||
const ImVec4 nord3 = RGBA(0x4C566A);
|
||||
const ImVec4 nord4 = RGBA(0xD8DEE9);
|
||||
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
|
||||
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
|
||||
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
|
||||
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
|
||||
const ImVec4 nord12 = RGBA(0xD08770); // orange
|
||||
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// Base style tweaks to suit Nord aesthetics
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
|
||||
colors[ImGuiCol_Text] = nord4; // primary text
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = nord0;
|
||||
colors[ImGuiCol_ChildBg] = nord0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = nord2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = nord2;
|
||||
colors[ImGuiCol_FrameBgHovered] = nord3;
|
||||
colors[ImGuiCol_FrameBgActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = nord1;
|
||||
colors[ImGuiCol_TitleBgActive] = nord2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = nord1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = nord1;
|
||||
colors[ImGuiCol_ScrollbarBg] = nord10;
|
||||
colors[ImGuiCol_ScrollbarGrab] = nord3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = nord2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = nord8;
|
||||
colors[ImGuiCol_SliderGrab] = nord8;
|
||||
colors[ImGuiCol_SliderGrabActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_Button] = nord3;
|
||||
colors[ImGuiCol_ButtonHovered] = nord2;
|
||||
colors[ImGuiCol_ButtonActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_Header] = nord3;
|
||||
colors[ImGuiCol_HeaderHovered] = nord10;
|
||||
colors[ImGuiCol_HeaderActive] = nord10;
|
||||
|
||||
colors[ImGuiCol_Separator] = nord2;
|
||||
colors[ImGuiCol_SeparatorHovered] = nord10;
|
||||
colors[ImGuiCol_SeparatorActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_Tab] = nord2;
|
||||
colors[ImGuiCol_TabHovered] = nord10;
|
||||
colors[ImGuiCol_TabActive] = nord3;
|
||||
colors[ImGuiCol_TabUnfocused] = nord2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = nord3;
|
||||
|
||||
// Docking colors omitted for compatibility
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = nord2;
|
||||
colors[ImGuiCol_TableBorderStrong] = nord1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
|
||||
colors[ImGuiCol_DragDropTarget] = nord13;
|
||||
colors[ImGuiCol_NavHighlight] = nord9;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||
|
||||
// Plots
|
||||
colors[ImGuiCol_PlotLines] = nord8;
|
||||
colors[ImGuiCol_PlotLinesHovered] = nord9;
|
||||
colors[ImGuiCol_PlotHistogram] = nord13;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = nord12;
|
||||
}
|
||||
89
themes/Plan9.h
Normal file
89
themes/Plan9.h
Normal file
@@ -0,0 +1,89 @@
|
||||
// themes/Plan9.h — Plan 9 acme-inspired ImGui theme (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyPlan9Theme()
|
||||
{
|
||||
// Acme-like colors
|
||||
const ImVec4 paper = RGBA(0xFFFFE8); // pale yellow paper
|
||||
const ImVec4 pane = RGBA(0xFFF4C1); // slightly deeper for frames
|
||||
const ImVec4 ink = RGBA(0x000000); // black text
|
||||
constexpr auto dim = ImVec4(0, 0, 0, 0.60f);
|
||||
const ImVec4 border = RGBA(0x000000); // 1px black
|
||||
const ImVec4 blue = RGBA(0x0064FF); // acme-ish blue accents
|
||||
const ImVec4 blueH = RGBA(0x4C8DFF); // hover/active
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(6.0f, 6.0f);
|
||||
style.FramePadding = ImVec2(5.0f, 3.0f);
|
||||
style.CellPadding = ImVec2(5.0f, 3.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *c = style.Colors;
|
||||
c[ImGuiCol_Text] = ink;
|
||||
c[ImGuiCol_TextDisabled] = dim;
|
||||
c[ImGuiCol_WindowBg] = paper;
|
||||
c[ImGuiCol_ChildBg] = paper;
|
||||
c[ImGuiCol_PopupBg] = ImVec4(pane.x, pane.y, pane.z, 0.98f);
|
||||
c[ImGuiCol_Border] = border;
|
||||
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
c[ImGuiCol_FrameBg] = pane;
|
||||
c[ImGuiCol_FrameBgHovered] = RGBA(0xFFEBA0);
|
||||
c[ImGuiCol_FrameBgActive] = RGBA(0xFFE387);
|
||||
c[ImGuiCol_TitleBg] = pane;
|
||||
c[ImGuiCol_TitleBgActive] = RGBA(0xFFE8A6);
|
||||
c[ImGuiCol_TitleBgCollapsed] = pane;
|
||||
c[ImGuiCol_MenuBarBg] = pane;
|
||||
c[ImGuiCol_ScrollbarBg] = paper;
|
||||
c[ImGuiCol_ScrollbarGrab] = RGBA(0xEADFA5);
|
||||
c[ImGuiCol_ScrollbarGrabHovered] = RGBA(0xE2D37F);
|
||||
c[ImGuiCol_ScrollbarGrabActive] = RGBA(0xD8C757);
|
||||
c[ImGuiCol_CheckMark] = blue;
|
||||
c[ImGuiCol_SliderGrab] = blue;
|
||||
c[ImGuiCol_SliderGrabActive] = blueH;
|
||||
c[ImGuiCol_Button] = RGBA(0xFFF1B0);
|
||||
c[ImGuiCol_ButtonHovered] = RGBA(0xFFE892);
|
||||
c[ImGuiCol_ButtonActive] = RGBA(0xFFE072);
|
||||
c[ImGuiCol_Header] = RGBA(0xFFF1B0);
|
||||
c[ImGuiCol_HeaderHovered] = RGBA(0xFFE892);
|
||||
c[ImGuiCol_HeaderActive] = RGBA(0xFFE072);
|
||||
c[ImGuiCol_Separator] = border;
|
||||
c[ImGuiCol_SeparatorHovered] = blue;
|
||||
c[ImGuiCol_SeparatorActive] = blueH;
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(0, 0, 0, 0.12f);
|
||||
c[ImGuiCol_ResizeGripHovered] = ImVec4(blue.x, blue.y, blue.z, 0.67f);
|
||||
c[ImGuiCol_ResizeGripActive] = blueH;
|
||||
c[ImGuiCol_Tab] = RGBA(0xFFE8A6);
|
||||
c[ImGuiCol_TabHovered] = RGBA(0xFFE072);
|
||||
c[ImGuiCol_TabActive] = RGBA(0xFFD859);
|
||||
c[ImGuiCol_TabUnfocused] = RGBA(0xFFE8A6);
|
||||
c[ImGuiCol_TabUnfocusedActive] = RGBA(0xFFD859);
|
||||
c[ImGuiCol_TableHeaderBg] = RGBA(0xFFE8A6);
|
||||
c[ImGuiCol_TableBorderStrong] = border;
|
||||
c[ImGuiCol_TableBorderLight] = ImVec4(0, 0, 0, 0.35f);
|
||||
c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0.04f);
|
||||
c[ImGuiCol_TableRowBgAlt] = ImVec4(0, 0, 0, 0.08f);
|
||||
c[ImGuiCol_TextSelectedBg] = ImVec4(blueH.x, blueH.y, blueH.z, 0.35f);
|
||||
c[ImGuiCol_DragDropTarget] = blue;
|
||||
c[ImGuiCol_NavHighlight] = blue;
|
||||
c[ImGuiCol_NavWindowingHighlight] = ImVec4(0, 0, 0, 0.20f);
|
||||
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0.20f);
|
||||
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0.20f);
|
||||
c[ImGuiCol_PlotLines] = blue;
|
||||
c[ImGuiCol_PlotLinesHovered] = blueH;
|
||||
c[ImGuiCol_PlotHistogram] = blue;
|
||||
c[ImGuiCol_PlotHistogramHovered] = blueH;
|
||||
}
|
||||
184
themes/Solarized.h
Normal file
184
themes/Solarized.h
Normal file
@@ -0,0 +1,184 @@
|
||||
// themes/Solarized.h — Solarized Dark/Light ImGui themes (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplySolarizedDarkTheme()
|
||||
{
|
||||
// Base colors from Ethan Schoonover Solarized
|
||||
const ImVec4 base03 = RGBA(0x002b36);
|
||||
const ImVec4 base02 = RGBA(0x073642);
|
||||
const ImVec4 base01 = RGBA(0x586e75);
|
||||
const ImVec4 base00 = RGBA(0x657b83);
|
||||
const ImVec4 base0 = RGBA(0x839496);
|
||||
const ImVec4 base1 = RGBA(0x93a1a1);
|
||||
const ImVec4 base2 = RGBA(0xeee8d5);
|
||||
const ImVec4 yellow = RGBA(0xb58900);
|
||||
const ImVec4 orange = RGBA(0xcb4b16);
|
||||
const ImVec4 blue = RGBA(0x268bd2);
|
||||
const ImVec4 cyan = RGBA(0x2aa198);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 3.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 3.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 3.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *c = style.Colors;
|
||||
c[ImGuiCol_Text] = base0;
|
||||
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
|
||||
c[ImGuiCol_WindowBg] = base03;
|
||||
c[ImGuiCol_ChildBg] = base03;
|
||||
c[ImGuiCol_PopupBg] = ImVec4(base02.x, base02.y, base02.z, 0.98f);
|
||||
c[ImGuiCol_Border] = base02;
|
||||
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
c[ImGuiCol_FrameBg] = base02;
|
||||
c[ImGuiCol_FrameBgHovered] = base01;
|
||||
c[ImGuiCol_FrameBgActive] = base00;
|
||||
c[ImGuiCol_TitleBg] = base02;
|
||||
c[ImGuiCol_TitleBgActive] = base01;
|
||||
c[ImGuiCol_TitleBgCollapsed] = base02;
|
||||
c[ImGuiCol_MenuBarBg] = base02;
|
||||
c[ImGuiCol_ScrollbarBg] = base02;
|
||||
c[ImGuiCol_ScrollbarGrab] = base01;
|
||||
c[ImGuiCol_ScrollbarGrabHovered] = base00;
|
||||
c[ImGuiCol_ScrollbarGrabActive] = blue;
|
||||
c[ImGuiCol_CheckMark] = cyan;
|
||||
c[ImGuiCol_SliderGrab] = cyan;
|
||||
c[ImGuiCol_SliderGrabActive] = blue;
|
||||
c[ImGuiCol_Button] = base01;
|
||||
c[ImGuiCol_ButtonHovered] = base00;
|
||||
c[ImGuiCol_ButtonActive] = blue;
|
||||
c[ImGuiCol_Header] = base01;
|
||||
c[ImGuiCol_HeaderHovered] = base00;
|
||||
c[ImGuiCol_HeaderActive] = base00;
|
||||
c[ImGuiCol_Separator] = base01;
|
||||
c[ImGuiCol_SeparatorHovered] = base00;
|
||||
c[ImGuiCol_SeparatorActive] = blue;
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
|
||||
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
|
||||
c[ImGuiCol_ResizeGripActive] = blue;
|
||||
c[ImGuiCol_Tab] = base01;
|
||||
c[ImGuiCol_TabHovered] = base00;
|
||||
c[ImGuiCol_TabActive] = base02;
|
||||
c[ImGuiCol_TabUnfocused] = base01;
|
||||
c[ImGuiCol_TabUnfocusedActive] = base02;
|
||||
c[ImGuiCol_TableHeaderBg] = base01;
|
||||
c[ImGuiCol_TableBorderStrong] = base00;
|
||||
c[ImGuiCol_TableBorderLight] = ImVec4(base00.x, base00.y, base00.z, 0.6f);
|
||||
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
|
||||
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
|
||||
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
|
||||
c[ImGuiCol_DragDropTarget] = yellow;
|
||||
c[ImGuiCol_NavHighlight] = blue;
|
||||
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
|
||||
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||
c[ImGuiCol_PlotLines] = cyan;
|
||||
c[ImGuiCol_PlotLinesHovered] = blue;
|
||||
c[ImGuiCol_PlotHistogram] = yellow;
|
||||
c[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
ApplySolarizedLightTheme()
|
||||
{
|
||||
// Base colors from Ethan Schoonover Solarized (light variant)
|
||||
const ImVec4 base3 = RGBA(0xfdf6e3);
|
||||
const ImVec4 base2 = RGBA(0xeee8d5);
|
||||
const ImVec4 base1 = RGBA(0x93a1a1);
|
||||
const ImVec4 base0 = RGBA(0x839496);
|
||||
const ImVec4 base00 = RGBA(0x657b83);
|
||||
const ImVec4 base01 = RGBA(0x586e75);
|
||||
const ImVec4 base02 = RGBA(0x073642);
|
||||
const ImVec4 base03 = RGBA(0x002b36);
|
||||
const ImVec4 yellow = RGBA(0xb58900);
|
||||
const ImVec4 orange = RGBA(0xcb4b16);
|
||||
const ImVec4 blue = RGBA(0x268bd2);
|
||||
const ImVec4 cyan = RGBA(0x2aa198);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 3.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 3.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 3.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *c = style.Colors;
|
||||
c[ImGuiCol_Text] = base00;
|
||||
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
|
||||
c[ImGuiCol_WindowBg] = base3;
|
||||
c[ImGuiCol_ChildBg] = base3;
|
||||
c[ImGuiCol_PopupBg] = ImVec4(base2.x, base2.y, base2.z, 0.98f);
|
||||
c[ImGuiCol_Border] = base1;
|
||||
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
c[ImGuiCol_FrameBg] = base2;
|
||||
c[ImGuiCol_FrameBgHovered] = base1;
|
||||
c[ImGuiCol_FrameBgActive] = base0;
|
||||
c[ImGuiCol_TitleBg] = base2;
|
||||
c[ImGuiCol_TitleBgActive] = base1;
|
||||
c[ImGuiCol_TitleBgCollapsed] = base2;
|
||||
c[ImGuiCol_MenuBarBg] = base2;
|
||||
c[ImGuiCol_ScrollbarBg] = base2;
|
||||
c[ImGuiCol_ScrollbarGrab] = base1;
|
||||
c[ImGuiCol_ScrollbarGrabHovered] = base0;
|
||||
c[ImGuiCol_ScrollbarGrabActive] = blue;
|
||||
c[ImGuiCol_CheckMark] = cyan;
|
||||
c[ImGuiCol_SliderGrab] = cyan;
|
||||
c[ImGuiCol_SliderGrabActive] = blue;
|
||||
c[ImGuiCol_Button] = base1;
|
||||
c[ImGuiCol_ButtonHovered] = base0;
|
||||
c[ImGuiCol_ButtonActive] = blue;
|
||||
c[ImGuiCol_Header] = base1;
|
||||
c[ImGuiCol_HeaderHovered] = base0;
|
||||
c[ImGuiCol_HeaderActive] = base0;
|
||||
c[ImGuiCol_Separator] = base1;
|
||||
c[ImGuiCol_SeparatorHovered] = base0;
|
||||
c[ImGuiCol_SeparatorActive] = blue;
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
|
||||
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
|
||||
c[ImGuiCol_ResizeGripActive] = blue;
|
||||
c[ImGuiCol_Tab] = base1;
|
||||
c[ImGuiCol_TabHovered] = base0;
|
||||
c[ImGuiCol_TabActive] = base2;
|
||||
c[ImGuiCol_TabUnfocused] = base1;
|
||||
c[ImGuiCol_TabUnfocusedActive] = base2;
|
||||
c[ImGuiCol_TableHeaderBg] = base1;
|
||||
c[ImGuiCol_TableBorderStrong] = base0;
|
||||
c[ImGuiCol_TableBorderLight] = ImVec4(base0.x, base0.y, base0.z, 0.6f);
|
||||
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
|
||||
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
|
||||
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
|
||||
c[ImGuiCol_DragDropTarget] = yellow;
|
||||
c[ImGuiCol_NavHighlight] = blue;
|
||||
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
|
||||
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||
c[ImGuiCol_PlotLines] = cyan;
|
||||
c[ImGuiCol_PlotLinesHovered] = blue;
|
||||
c[ImGuiCol_PlotHistogram] = yellow;
|
||||
c[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
17
themes/ThemeHelpers.h
Normal file
17
themes/ThemeHelpers.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef KTE_THEMEHELPERS_H
|
||||
#define KTE_THEMEHELPERS_H
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
|
||||
static ImVec4
|
||||
RGBA(const unsigned int rgb, float a = 1.0f)
|
||||
{
|
||||
const float r = static_cast<float>(rgb >> 16 & 0xFF) / 255.0f;
|
||||
const float g = static_cast<float>(rgb >> 8 & 0xFF) / 255.0f;
|
||||
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
|
||||
return {r, g, b, a};
|
||||
}
|
||||
|
||||
|
||||
#endif //KTE_THEMEHELPERS_H
|
||||
Reference in New Issue
Block a user