5 Commits

Author SHA1 Message Date
1a77f28ce4 Add syntax highlighting infrastructure
- Introduced `HighlighterRegistry` with support for multiple language highlighters (e.g., JSON, Markdown, Python).
- Added `JsonHighlighter` implementation for basic JSON syntax highlighting.
2025-12-01 18:20:36 -08:00
4d84b352eb format style 2025-12-01 16:47:16 -08:00
1892075d82 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-01 16:38:10 -08:00
719862c842 Fix double scroll issue. 2025-12-01 16:37:51 -08:00
655cc40162 Refine help text, keybindings, GUI themes, and undo system.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
- Expanded help text and command documentation with detailed keybinding descriptions.
- Added theme customization support to GUIConfig (Nord default, light/dark variants).
- Adjusted for consistent indentation and debug instrumentation in undo system.
- Enhanced test cases for multi-line, UTF-8, and branching scenarios.
2025-12-01 15:21:52 -08:00
51 changed files with 4721 additions and 1875 deletions

369
.idea/workspace.xml generated
View File

@@ -1,369 +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">{
&quot;useNewFormat&quot;: 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 &quot;Unix Makefiles&quot; -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="Add Nord theme for real">
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" 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" />
<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="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" />
</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">{
&quot;associatedIndex&quot;: 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"><![CDATA[{
"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": "CMakeSettings",
"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="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" 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" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" 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="39312000" />
</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.&#10;&#10;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.&#10;&#10;- Document `TestFrontend` for programmatic testing, including examples and usage details.&#10;- 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.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- 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.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- 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.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- 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.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- 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.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- 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.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- 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.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- 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>
<task id="LOCAL-00015" summary="Fix void crash in kge.">
<option name="closed" value="true" />
<created>1764568264996</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1764568264996</updated>
</task>
<task id="LOCAL-00016" summary="add regex and search/replace functionality to editor">
<option name="closed" value="true" />
<created>1764574397967</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1764574397967</updated>
</task>
<task id="LOCAL-00017" summary="Add regex search, search/replace, and buffer read-only mode functionality with help text">
<option name="closed" value="true" />
<created>1764586480092</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1764586480092</updated>
</task>
<task id="LOCAL-00018" summary="Nord theme and undo system refinements&#10;&#10;- Improve text input/event batching&#10;- Enhance debugging with optional instrumentation&#10;- Begin implementation of non-linear undo tree structure.">
<option name="closed" value="true" />
<created>1764619193516</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1764619193517</updated>
</task>
<task id="LOCAL-00019" summary="Add Nord theme for real">
<option name="closed" value="true" />
<created>1764619210817</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1764619210817</updated>
</task>
<option name="localTasksCounter" value="20" />
<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.&#10;&#10;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.&#10;&#10;- Document `TestFrontend` for programmatic testing, including examples and usage details.&#10;- Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." />
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." />
<MESSAGE value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." />
<MESSAGE value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- Replace redundant variable declarations in `Buffer` for cleaner code." />
<MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." />
<MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." />
<MESSAGE value="Actually add the screenshot." />
<MESSAGE value="Fix void crash in kge." />
<MESSAGE value="add regex and search/replace functionality to editor" />
<MESSAGE value="Add regex search, search/replace, and buffer read-only mode functionality with help text" />
<MESSAGE value="Nord theme and undo system refinements&#10;&#10;- Improve text input/event batching&#10;- Enhance debugging with optional instrumentation&#10;- Begin implementation of non-linear undo tree structure." />
<MESSAGE value="Add Nord theme for real" />
<option name="LAST_COMMIT_MESSAGE" value="Add Nord theme for real" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

View File

@@ -12,6 +12,10 @@
#include "AppendBuffer.h" #include "AppendBuffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include <cstdint>
#include <memory>
#include "HighlighterEngine.h"
#include "Highlight.h"
class Buffer { class Buffer {
@@ -262,6 +266,7 @@ public:
return filename_; return filename_;
} }
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+" // Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
// This does not mark the buffer as file-backed. // This does not mark the buffer as file-backed.
void SetVirtualName(const std::string &name) void SetVirtualName(const std::string &name)
@@ -282,17 +287,20 @@ public:
return dirty_; return dirty_;
} }
// Read-only flag // Read-only flag
[[nodiscard]] bool IsReadOnly() const [[nodiscard]] bool IsReadOnly() const
{ {
return read_only_; return read_only_;
} }
void SetReadOnly(bool ro) void SetReadOnly(bool ro)
{ {
read_only_ = ro; read_only_ = ro;
} }
void ToggleReadOnly() void ToggleReadOnly()
{ {
read_only_ = !read_only_; read_only_ = !read_only_;
@@ -322,6 +330,12 @@ public:
void SetDirty(bool d) void SetDirty(bool d)
{ {
dirty_ = d; dirty_ = d;
if (d) {
++version_;
if (highlighter_) {
highlighter_->InvalidateFrom(0);
}
}
} }
@@ -360,6 +374,23 @@ public:
[[nodiscard]] std::string AsString() const; [[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(). // Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor. // These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text); void insert_text(int row, int col, std::string_view text);
@@ -396,6 +427,12 @@ private:
// Per-buffer undo state // Per-buffer undo state
std::unique_ptr<struct UndoTree> undo_tree_; std::unique_ptr<struct UndoTree> undo_tree_;
std::unique_ptr<UndoSystem> undo_sys_; 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 #endif // KTE_BUFFER_H

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.1.0") set(KTE_VERSION "1.1.2")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -67,6 +67,17 @@ set(COMMON_SOURCES
UndoNode.cc UndoNode.cc
UndoTree.cc UndoTree.cc
UndoSystem.cc UndoSystem.cc
HighlighterEngine.cc
CppHighlighter.cc
HighlighterRegistry.cc
NullHighlighter.cc
JsonHighlighter.cc
MarkdownHighlighter.cc
ShellHighlighter.cc
GoHighlighter.cc
PythonHighlighter.cc
RustHighlighter.cc
LispHighlighter.cc
) )
set(COMMON_HEADERS set(COMMON_HEADERS
@@ -90,6 +101,19 @@ set(COMMON_HEADERS
UndoNode.h UndoNode.h
UndoTree.h UndoTree.h
UndoSystem.h UndoSystem.h
Highlight.h
LanguageHighlighter.h
HighlighterEngine.h
CppHighlighter.h
HighlighterRegistry.h
NullHighlighter.h
JsonHighlighter.h
MarkdownHighlighter.h
ShellHighlighter.h
GoHighlighter.h
PythonHighlighter.h
RustHighlighter.h
LispHighlighter.h
) )
# kte (terminal-first) executable # kte (terminal-first) executable

View File

@@ -4,12 +4,21 @@
#include <regex> #include <regex>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cctype>
#include "Command.h" #include "Command.h"
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#include "Editor.h" #include "Editor.h"
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include "HelpText.h" #include "HelpText.h"
#include "LanguageHighlighter.h"
#include "HighlighterEngine.h"
#include "CppHighlighter.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#endif
// Keep buffer viewport offsets so that the cursor stays within the visible // Keep buffer viewport offsets so that the cursor stays within the visible
@@ -87,8 +96,10 @@ ensure_at_least_one_line(Buffer &buf)
} }
} }
// Determine if a command mutates the buffer contents (text edits) // Determine if a command mutates the buffer contents (text edits)
static bool is_mutating_command(CommandId id) static bool
is_mutating_command(CommandId id)
{ {
switch (id) { switch (id) {
case CommandId::InsertText: case CommandId::InsertText:
@@ -723,6 +734,21 @@ cmd_kprefix(CommandContext &ctx)
} }
// Start generic command prompt (": ")
static bool
cmd_command_prompt_start(const CommandContext &ctx)
{
// Close any pending edit batch before entering prompt
if (Buffer *b = ctx.editor.CurrentBuffer()) {
if (auto *u = b->Undo())
u->commit();
}
ctx.editor.StartPrompt(Editor::PromptKind::Command, "", "");
ctx.editor.SetStatus(": ");
return true;
}
static bool static bool
cmd_unknown_kcommand(CommandContext &ctx) cmd_unknown_kcommand(CommandContext &ctx)
{ {
@@ -736,9 +762,254 @@ cmd_unknown_kcommand(CommandContext &ctx)
return true; return true;
} }
// --- Syntax highlighting commands ---
static void apply_filetype(Buffer &buf, const std::string &ft)
{
buf.EnsureHighlighter();
auto *eng = buf.Highlighter();
if (!eng) return;
std::string val = ft;
// trim + lower
auto trim = [](const std::string &s){
std::string r = s;
auto notsp = [](int ch){ return !std::isspace(ch); };
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
return r;
};
val = trim(val);
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (val == "off") {
eng->SetHighlighter(nullptr);
buf.SetFiletype("");
buf.SetSyntaxEnabled(false);
return;
}
if (val.empty()) {
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
buf.SetFiletype("");
buf.SetSyntaxEnabled(true);
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
return;
}
// Normalize and create via registry
std::string norm = kte::HighlighterRegistry::Normalize(val);
auto hl = kte::HighlighterRegistry::CreateFor(norm);
if (hl) {
eng->SetHighlighter(std::move(hl));
buf.SetFiletype(norm);
buf.SetSyntaxEnabled(true);
eng->InvalidateFrom(0);
} else {
// Unknown -> install NullHighlighter and keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
buf.SetFiletype(val); // record what user asked even if unsupported
buf.SetSyntaxEnabled(true);
eng->InvalidateFrom(0);
}
}
static bool cmd_syntax(CommandContext &ctx)
{
Buffer *b = ctx.editor.CurrentBuffer();
if (!b) {
ctx.editor.SetStatus("No buffer");
return true;
}
std::string arg = ctx.arg;
// trim
auto trim = [](std::string &s){
auto notsp = [](int ch){ return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
};
trim(arg);
if (arg == "on") {
b->SetSyntaxEnabled(true);
// If no highlighter but filetype is cpp by extension, set it
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
}
ctx.editor.SetStatus("syntax: on");
} else if (arg == "off") {
b->SetSyntaxEnabled(false);
ctx.editor.SetStatus("syntax: off");
} else if (arg == "reload") {
if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0);
ctx.editor.SetStatus("syntax: reloaded");
} else {
ctx.editor.SetStatus("usage: :syntax on|off|reload");
}
return true;
}
static bool cmd_set_option(CommandContext &ctx)
{
Buffer *b = ctx.editor.CurrentBuffer();
if (!b) {
ctx.editor.SetStatus("No buffer");
return true;
}
// Expect key=value
auto eq = ctx.arg.find('=');
if (eq == std::string::npos) {
ctx.editor.SetStatus("usage: :set key=value");
return true;
}
std::string key = ctx.arg.substr(0, eq);
std::string val = ctx.arg.substr(eq + 1);
// trim
auto trim = [](std::string &s){
auto notsp = [](int ch){ return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
};
trim(key); trim(val);
// lower-case value for filetype
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (key == "filetype") {
apply_filetype(*b, val);
if (b->SyntaxEnabled())
ctx.editor.SetStatus(std::string("filetype: ") + (b->Filetype().empty()?"off":b->Filetype()));
else
ctx.editor.SetStatus("filetype: off");
return true;
}
ctx.editor.SetStatus("unknown option: " + key);
return true;
}
// GUI theme cycling commands (available in GUI build; show message otherwise)
#ifdef KTE_BUILD_GUI
static bool
cmd_theme_next(CommandContext &ctx)
{
auto id = kte::NextTheme();
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
return true;
}
static bool static bool
cmd_find_start(CommandContext &ctx) cmd_theme_prev(CommandContext &ctx)
{
auto id = kte::PrevTheme();
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
return true;
}
#else
static bool
cmd_theme_next(CommandContext &ctx)
{
ctx.editor.SetStatus("Theme switching only available in GUI build");
return true;
}
static bool
cmd_theme_prev(CommandContext &ctx)
{
ctx.editor.SetStatus("Theme switching only available in GUI build");
return true;
}
#endif
// Theme set by name command
#ifdef KTE_BUILD_GUI
static bool
cmd_theme_set_by_name(const CommandContext &ctx)
{
std::string name = ctx.arg;
// trim spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(name);
rtrim(name);
if (name.empty()) {
ctx.editor.SetStatus("theme: missing name");
return true;
}
if (kte::ApplyThemeByName(name)) {
ctx.editor.SetStatus(
std::string("Theme: ") + name + std::string(" (bg: ") + kte::BackgroundModeName() + ")");
} else {
// Build list of available themes
const auto &reg = kte::ThemeRegistry();
std::string avail;
for (size_t i = 0; i < reg.size(); ++i) {
if (i)
avail += ", ";
avail += reg[i]->Name();
}
ctx.editor.SetStatus(std::string("Unknown theme; available: ") + avail);
}
return true;
}
#else
static bool
cmd_theme_set_by_name(CommandContext &ctx)
{
(void) ctx;
// No-op in terminal build
return true;
}
#endif
// Background set command (GUI)
#ifdef KTE_BUILD_GUI
static bool
cmd_background_set(const CommandContext &ctx)
{
std::string mode = ctx.arg;
// trim
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(mode);
rtrim(mode);
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (mode != "light" && mode != "dark") {
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
return true;
}
kte::SetBackgroundMode(mode == "light" ? kte::BackgroundMode::Light : kte::BackgroundMode::Dark);
// Re-apply current theme to reflect background change
kte::ApplyThemeByName(kte::CurrentThemeName());
ctx.editor.SetStatus(std::string("Background: ") + mode + std::string("; Theme: ") + kte::CurrentThemeName());
return true;
}
#else
static bool
cmd_background_set(CommandContext &ctx)
{
(void) ctx;
return true;
}
#endif
static bool
cmd_find_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -761,7 +1032,7 @@ cmd_find_start(CommandContext &ctx)
static bool static bool
cmd_regex_find_start(CommandContext &ctx) cmd_regex_find_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -784,7 +1055,7 @@ cmd_regex_find_start(CommandContext &ctx)
static bool static bool
cmd_search_replace_start(CommandContext &ctx) cmd_search_replace_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -808,7 +1079,7 @@ cmd_search_replace_start(CommandContext &ctx)
static bool static bool
cmd_regex_replace_start(CommandContext &ctx) cmd_regex_replace_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -832,7 +1103,7 @@ cmd_regex_replace_start(CommandContext &ctx)
static bool static bool
cmd_open_file_start(CommandContext &ctx) cmd_open_file_start(const CommandContext &ctx)
{ {
// Start a generic prompt to read a path // Start a generic prompt to read a path
ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", ""); ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", "");
@@ -843,7 +1114,7 @@ cmd_open_file_start(CommandContext &ctx)
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag) // GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
static bool static bool
cmd_visual_file_picker_toggle(CommandContext &ctx) cmd_visual_file_picker_toggle(const CommandContext &ctx)
{ {
// Toggle visibility // Toggle visibility
bool show = !ctx.editor.FilePickerVisible(); bool show = !ctx.editor.FilePickerVisible();
@@ -866,7 +1137,7 @@ cmd_visual_file_picker_toggle(CommandContext &ctx)
static bool static bool
cmd_jump_to_line_start(CommandContext &ctx) cmd_jump_to_line_start(const CommandContext &ctx)
{ {
// Start a prompt to read a 1-based line number and jump there (clamped) // Start a prompt to read a 1-based line number and jump there (clamped)
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", ""); ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
@@ -877,7 +1148,7 @@ cmd_jump_to_line_start(CommandContext &ctx)
// --- Buffers: switch/next/prev/close --- // --- Buffers: switch/next/prev/close ---
static bool static bool
cmd_buffer_switch_start(CommandContext &ctx) cmd_buffer_switch_start(const CommandContext &ctx)
{ {
// If only one (or zero) buffer is open, do nothing per spec // If only one (or zero) buffer is open, do nothing per spec
if (ctx.editor.BufferCount() <= 1) { if (ctx.editor.BufferCount() <= 1) {
@@ -895,7 +1166,7 @@ buffer_display_name(const Buffer &b)
{ {
if (!b.Filename().empty()) if (!b.Filename().empty())
return b.Filename(); return b.Filename();
return std::string("<untitled>"); return {"<untitled>"};
} }
@@ -904,7 +1175,7 @@ buffer_basename(const Buffer &b)
{ {
const std::string &p = b.Filename(); const std::string &p = b.Filename();
if (p.empty()) if (p.empty())
return std::string("<untitled>"); return {"<untitled>"};
auto pos = p.find_last_of("/\\"); auto pos = p.find_last_of("/\\");
if (pos == std::string::npos) if (pos == std::string::npos)
return p; return p;
@@ -913,7 +1184,7 @@ buffer_basename(const Buffer &b)
static bool static bool
cmd_buffer_next(CommandContext &ctx) cmd_buffer_next(const CommandContext &ctx)
{ {
const auto cnt = ctx.editor.BufferCount(); const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) { if (cnt <= 1) {
@@ -930,7 +1201,7 @@ cmd_buffer_next(CommandContext &ctx)
static bool static bool
cmd_buffer_prev(CommandContext &ctx) cmd_buffer_prev(const CommandContext &ctx)
{ {
const auto cnt = ctx.editor.BufferCount(); const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) { if (cnt <= 1) {
@@ -947,7 +1218,7 @@ cmd_buffer_prev(CommandContext &ctx)
static bool static bool
cmd_buffer_close(CommandContext &ctx) cmd_buffer_close(const CommandContext &ctx)
{ {
if (ctx.editor.BufferCount() == 0) if (ctx.editor.BufferCount() == 0)
return true; return true;
@@ -1116,6 +1387,85 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true; return true;
} }
// Generic command prompt completion
if (kind == Editor::PromptKind::Command) {
std::string text = ctx.editor.PromptText();
// Split into command and arg prefix
auto sp = text.find(' ');
if (sp == std::string::npos) {
// complete command name from public commands
std::string prefix = text;
std::vector<std::string> names;
for (const auto &c: CommandRegistry::All()) {
if (c.isPublic) {
if (prefix.empty() || c.name.rfind(prefix, 0) == 0)
names.push_back(c.name);
}
}
if (names.empty()) {
// no change
} else if (names.size() == 1) {
ctx.editor.SetPromptText(names[0]);
} else {
// compute LCP
std::string lcp = names[0];
for (size_t i = 1; i < names.size(); ++i) {
const std::string &s = names[i];
size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
}
if (!lcp.empty() && lcp != text)
ctx.editor.SetPromptText(lcp);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
} else {
std::string cmd = text.substr(0, sp);
std::string argprefix = text.substr(sp + 1);
// Only special-case argument completion for certain commands
if (cmd == "theme") {
#ifdef KTE_BUILD_GUI
std::vector<std::string> cands;
const auto &reg = kte::ThemeRegistry();
for (const auto &t: reg) {
std::string n = t->Name();
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
cands.push_back(n);
}
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
} else {
std::string lcp = cands[0];
for (size_t i = 1; i < cands.size(); ++i) {
const std::string &s = cands[i];
size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
}
if (!lcp.empty() && lcp != argprefix)
ctx.editor.SetPromptText(cmd + std::string(" ") + lcp);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
#else
(void) argprefix; // no completion in non-GUI build
#endif
}
// default: no special arg completion
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
}
}
} }
ctx.editor.AppendPromptText(ctx.arg); ctx.editor.AppendPromptText(ctx.arg);
@@ -1223,6 +1573,7 @@ cmd_insert_text(CommandContext &ctx)
return true; return true;
} }
// Toggle read-only state of the current buffer // Toggle read-only state of the current buffer
static bool static bool
cmd_toggle_read_only(CommandContext &ctx) cmd_toggle_read_only(CommandContext &ctx)
@@ -1258,8 +1609,10 @@ cmd_show_help(CommandContext &ctx)
std::ostringstream out; std::ostringstream out;
std::string line; std::string line;
auto unquote = [](std::string s) { auto unquote = [](std::string s) {
if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin()); if (!s.empty() && (s.front() == '"' || s.front() == '\''))
if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back(); s.erase(s.begin());
if (!s.empty() && (s.back() == '"' || s.back() == '\''))
s.pop_back();
return s; return s;
}; };
while (std::getline(iss, line)) { while (std::getline(iss, line)) {
@@ -1275,17 +1628,20 @@ cmd_show_help(CommandContext &ctx)
std::string title; std::string title;
std::getline(ls, title); std::getline(ls, title);
// trim leading spaces // trim leading spaces
while (!title.empty() && (title.front() == ' ' || title.front() == '\t')) title.erase(title.begin()); while (!title.empty() && (title.front() == ' ' || title.front() == '\t'))
title.erase(title.begin());
title = unquote(title); title = unquote(title);
out << "\n\n"; out << "\n\n";
for (auto &c : title) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c))); for (auto &c: title)
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
out << title << "\n"; out << title << "\n";
} else if (macro == "PP" || macro == "P" || macro == "TP") { } else if (macro == "PP" || macro == "P" || macro == "TP") {
out << "\n"; out << "\n";
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") { } else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
std::string rest; std::string rest;
std::getline(ls, rest); std::getline(ls, rest);
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) rest.erase(rest.begin()); while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
rest.erase(rest.begin());
out << unquote(rest) << "\n"; out << unquote(rest) << "\n";
} else if (macro == "nf" || macro == "fi") { } else if (macro == "nf" || macro == "fi") {
// ignore fill mode toggles for now // ignore fill mode toggles for now
@@ -1297,11 +1653,23 @@ cmd_show_help(CommandContext &ctx)
// Regular text; apply minimal escape replacements // Regular text; apply minimal escape replacements
for (std::size_t i = 0; i < line.size(); ++i) { for (std::size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\\') { if (line[i] == '\\') {
if (i + 1 < line.size() && line[i + 1] == '-') { out << '-'; ++i; continue; } if (i + 1 < line.size() && line[i + 1] == '-') {
out << '-';
++i;
continue;
}
if (i + 3 < line.size() && line[i + 1] == '(') { if (i + 3 < line.size() && line[i + 1] == '(') {
std::string esc = line.substr(i + 2, 2); std::string esc = line.substr(i + 2, 2);
if (esc == "em") { out << ""; i += 3; continue; } if (esc == "em") {
if (esc == "en") { out << "-"; i += 3; continue; } out << "";
i += 3;
continue;
}
if (esc == "en") {
out << "-";
i += 3;
continue;
}
} }
} }
out << line[i]; out << line[i];
@@ -1315,7 +1683,10 @@ cmd_show_help(CommandContext &ctx)
// 1) Prefer embedded/customizable help content // 1) Prefer embedded/customizable help content
{ {
std::string embedded = HelpText::Text(); std::string embedded = HelpText::Text();
if (!embedded.empty()) { used_man = false; return embedded; } if (!embedded.empty()) {
used_man = false;
return embedded;
}
} }
// 2) Fall back to the manpage and convert roff to plain text // 2) Fall back to the manpage and convert roff to plain text
@@ -1325,11 +1696,14 @@ cmd_show_help(CommandContext &ctx)
"/usr/local/share/man/man1/kte.1", "/usr/local/share/man/man1/kte.1",
"/usr/share/man/man1/kte.1" "/usr/share/man/man1/kte.1"
}; };
for (const char *p : man_candidates) { for (const char *p: man_candidates) {
std::ifstream in(p); std::ifstream in(p);
if (in.good()) { if (in.good()) {
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!s.empty()) { used_man = true; return roff_to_text(s); } if (!s.empty()) {
used_man = true;
return roff_to_text(s);
}
} }
} }
// Fallback minimal help text // Fallback minimal help text
@@ -1378,7 +1752,7 @@ cmd_show_help(CommandContext &ctx)
rows.clear(); rows.clear();
std::string line; std::string line;
line.reserve(128); line.reserve(128);
for (char ch : text) { for (char ch: text) {
if (ch == '\n') { if (ch == '\n') {
rows.emplace_back(line); rows.emplace_back(line);
line.clear(); line.clear();
@@ -1430,6 +1804,46 @@ cmd_newline(CommandContext &ctx)
Editor::PromptKind kind = ctx.editor.CurrentPromptKind(); Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
std::string value = ctx.editor.PromptText(); std::string value = ctx.editor.PromptText();
ctx.editor.AcceptPrompt(); ctx.editor.AcceptPrompt();
if (kind == Editor::PromptKind::Command) {
// Parse COMMAND ARG and dispatch only public commands
// Trim leading/trailing spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(value);
rtrim(value);
if (value.empty()) {
ctx.editor.SetStatus("Canceled");
return true;
}
// Split first token
std::string cmdname;
std::string arg;
auto sp = value.find(' ');
if (sp == std::string::npos) {
cmdname = value;
} else {
cmdname = value.substr(0, sp);
arg = value.substr(sp + 1);
}
const Command *cmd = CommandRegistry::FindByName(cmdname);
if (!cmd || !cmd->isPublic) {
ctx.editor.SetStatus(std::string("Unknown command: ") + cmdname);
return true;
}
bool ok = Execute(ctx.editor, cmdname, arg);
if (!ok) {
ctx.editor.SetStatus(std::string("Command failed: ") + cmdname);
}
return true;
}
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) { if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
// Finish search: keep cursor where it is, clear search UI prompt // Finish search: keep cursor where it is, clear search UI prompt
ctx.editor.SetSearchActive(false); ctx.editor.SetSearchActive(false);
@@ -1786,7 +2200,7 @@ cmd_newline(CommandContext &ctx)
} }
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t changed = 0; std::size_t changed = 0;
for (auto &line : rows) { for (auto &line: rows) {
std::string before = static_cast<std::string>(line); std::string before = static_cast<std::string>(line);
std::string after = std::regex_replace(before, rx, repl); std::string after = std::regex_replace(before, rx, repl);
if (after != before) { if (after != before) {
@@ -3275,7 +3689,25 @@ InstallDefaultCommands()
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
}); });
// Read-only // Read-only
CommandRegistry::Register({CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only}); CommandRegistry::Register({
CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only
});
// GUI Themes
CommandRegistry::Register({CommandId::ThemeNext, "theme-next", "Cycle to next GUI theme", cmd_theme_next});
CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev});
// Theme by name (public in command prompt)
CommandRegistry::Register({
CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true
});
// Background light/dark (public)
CommandRegistry::Register({
CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true
});
// Generic command prompt (C-k ;)
CommandRegistry::Register({
CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt",
cmd_command_prompt_start
});
// Buffer operations // Buffer operations
CommandRegistry::Register({ CommandRegistry::Register({
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
@@ -3305,6 +3737,9 @@ InstallDefaultCommands()
// UI helpers // UI helpers
CommandRegistry::Register( CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status}); {CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
// Syntax highlighting (public commands)
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
} }

View File

@@ -69,6 +69,9 @@ enum class CommandId {
Redo, Redo,
// UI/status helpers // UI/status helpers
UArgStatus, // update status line during universal-argument collection UArgStatus, // update status line during universal-argument collection
// Themes (GUI)
ThemeNext,
ThemePrev,
// Region formatting // Region formatting
IndentRegion, // indent region (C-k =) IndentRegion, // indent region (C-k =)
UnindentRegion, // unindent region (C-k -) UnindentRegion, // unindent region (C-k -)
@@ -86,6 +89,15 @@ enum class CommandId {
ShowHelp, // open +HELP+ buffer with manual text (C-k h) ShowHelp, // open +HELP+ buffer with manual text (C-k h)
// Meta // Meta
UnknownKCommand, // arg: single character that was not recognized after C-k 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>)
}; };
@@ -109,6 +121,8 @@ struct Command {
std::string name; // stable, unique name (e.g., "save", "save-as") std::string name; // stable, unique name (e.g., "save", "save-as")
std::string help; // short help/description std::string help; // short help/description
CommandHandler handler; CommandHandler handler;
// Public commands are exposed in the ": " prompt (C-k ;)
bool isPublic = false;
}; };

170
CppHighlighter.cc Normal file
View File

@@ -0,0 +1,170 @@
#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

34
CppHighlighter.h Normal file
View File

@@ -0,0 +1,34 @@
// 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

View File

@@ -3,6 +3,9 @@
#include <filesystem> #include <filesystem>
#include "Editor.h" #include "Editor.h"
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include "NullHighlighter.h"
Editor::Editor() = default; Editor::Editor() = default;
@@ -151,7 +154,30 @@ Editor::OpenFile(const std::string &path, std::string &err)
const bool rows_empty = rows.empty(); const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0); const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) { 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 +185,29 @@ Editor::OpenFile(const std::string &path, std::string &err)
if (!b.OpenFromFile(path, err)) { if (!b.OpenFromFile(path, err)) {
return false; 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 // Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b)); std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx); SwitchTo(idx);

View File

@@ -315,7 +315,8 @@ public:
GotoLine, GotoLine,
Chdir, Chdir,
ReplaceFind, // step 1 of Search & Replace: find what ReplaceFind, // step 1 of Search & Replace: find what
ReplaceWith // step 2 of Search & Replace: replace with ReplaceWith, // step 2 of Search & Replace: replace with
Command // generic command prompt (": ")
}; };
@@ -524,10 +525,28 @@ private:
// Temporary state for Search & Replace flow // Temporary state for Search & Replace flow
public: public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; } void SetReplaceFindTmp(const std::string &s)
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; } {
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; } replace_find_tmp_ = s;
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; } }
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: private:
std::string replace_find_tmp_; std::string replace_find_tmp_;

View File

@@ -102,6 +102,25 @@ GUIConfig::LoadFromFile(const std::string &path)
if (v > 0.0f) { if (v > 0.0f) {
font_size = v; 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;
}
} }
} }

View File

@@ -16,6 +16,14 @@ public:
int columns = 80; int columns = 80;
int rows = 42; int rows = 42;
float font_size = (float) KTE_FONT_SIZE; 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 // Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load(); static GUIConfig Load();

View File

@@ -16,6 +16,8 @@
#include "Font.h" // embedded default font (DefaultFontRegular) #include "Font.h" // embedded default font (DefaultFontRegular)
#include "GUIConfig.h" #include "GUIConfig.h"
#include "GUITheme.h" #include "GUITheme.h"
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#ifndef KTE_FONT_SIZE #ifndef KTE_FONT_SIZE
@@ -32,8 +34,8 @@ GUIFrontend::Init(Editor &ed)
return false; return false;
} }
// Load GUI configuration (fullscreen, columns/rows, font size) // Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load(); GUIConfig cfg = GUIConfig::Load();
// GL attributes for core profile // GL attributes for core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
@@ -47,7 +49,7 @@ GUIFrontend::Init(Editor &ed)
// Compute desired window size from config // Compute desired window size from config
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 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. // "Fullscreen": fill the usable bounds of the primary display.
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible. // On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
SDL_Rect usable{}; SDL_Rect usable{};
@@ -61,8 +63,8 @@ GUIFrontend::Init(Editor &ed)
#endif #endif
} else { } else {
// Windowed: width = columns * font_size, height = (rows * 2) * font_size // Windowed: width = columns * font_size, height = (rows * 2) * font_size
int w = static_cast<int>(columns * font_size); int w = cfg.columns * static_cast<int>(cfg.font_size);
int h = static_cast<int>((rows * 2) * font_size); int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
// As a safety, clamp to display usable bounds if retrievable // As a safety, clamp to display usable bounds if retrievable
SDL_Rect usable{}; SDL_Rect usable{};
@@ -86,7 +88,7 @@ GUIFrontend::Init(Editor &ed)
// macOS: when "fullscreen" is requested, position the window at the // macOS: when "fullscreen" is requested, position the window at the
// top-left of the usable display area to mimic fullscreen while keeping // top-left of the usable display area to mimic fullscreen while keeping
// the system menu bar visible. // the system menu bar visible.
if (fullscreen) { if (cfg.fullscreen) {
SDL_Rect usable{}; SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) { if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
SDL_SetWindowPosition(window_, usable.x, usable.y); SDL_SetWindowPosition(window_, usable.x, usable.y);
@@ -105,8 +107,43 @@ GUIFrontend::Init(Editor &ed)
ImGuiIO &io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
(void) io; (void) io;
ImGui::StyleColorsDark(); ImGui::StyleColorsDark();
// Apply a Nord-inspired theme
kte::ApplyNordImGuiTheme(); // 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_)) if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false; return false;
@@ -135,7 +172,7 @@ GUIFrontend::Init(Editor &ed)
#endif #endif
// Initialize GUI font from embedded default (use configured size or compiled default) // 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; return true;
} }
@@ -214,7 +251,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h); float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
// Visible content rows inside the scroll child // 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. // 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 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))); std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
@@ -264,11 +301,11 @@ GUIFrontend::Shutdown()
bool bool
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px) GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
{ {
ImGuiIO &io = ImGui::GetIO(); const ImGuiIO &io = ImGui::GetIO();
io.Fonts->Clear(); io.Fonts->Clear();
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
(void *) DefaultFontBoldCompressedData, DefaultFontBoldCompressedData,
(int) DefaultFontBoldCompressedSize, DefaultFontBoldCompressedSize,
size_px); size_px);
if (!font) { if (!font) {
font = io.Fonts->AddFontDefault(); font = io.Fonts->AddFontDefault();

View File

@@ -25,7 +25,7 @@ public:
void Shutdown() override; void Shutdown() override;
private: private:
bool LoadGuiFont_(const char *path, float size_px); static bool LoadGuiFont_(const char *path, float size_px);
GUIInputHandler input_{}; GUIInputHandler input_{};
GUIRenderer renderer_{}; GUIRenderer renderer_{};

View File

@@ -92,10 +92,14 @@ map_key(const SDL_Keycode key,
out = {true, CommandId::Backspace, "", 0}; out = {true, CommandId::Backspace, "", 0};
return true; return true;
case SDLK_TAB: case SDLK_TAB:
// Do not insert text on KEYDOWN; allow SDL_TEXTINPUT to deliver '\t' // Insert a literal tab character when not interpreting a k-prefix suffix.
// as printable input so that all printable characters flow via TEXTINPUT. // If k-prefix is active, let the k-prefix handler below consume the key
out.hasCommand = false; // (so Tab doesn't leave k-prefix stuck).
if (!k_prefix) {
out = {true, CommandId::InsertText, std::string("\t"), 0};
return true; return true;
}
break; // fall through so k-prefix handler can process
case SDLK_RETURN: case SDLK_RETURN:
case SDLK_KP_ENTER: case SDLK_KP_ENTER:
out = {true, CommandId::Newline, "", 0}; out = {true, CommandId::Newline, "", 0};
@@ -347,6 +351,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
uarg_text_, uarg_text_,
mi); 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, // 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. // suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) { if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {

View File

@@ -10,6 +10,8 @@
#include <regex> #include <regex>
#include "GUIRenderer.h" #include "GUIRenderer.h"
#include "Highlight.h"
#include "GUITheme.h"
#include "Buffer.h" #include "Buffer.h"
#include "Command.h" #include "Command.h"
#include "Editor.h" #include "Editor.h"
@@ -44,6 +46,8 @@ GUIRenderer::Draw(Editor &ed)
ImGui::SetNextWindowSize(main_sz); ImGui::SetNextWindowSize(main_sz);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoCollapse
@@ -254,10 +258,12 @@ GUIRenderer::Draw(Editor &ed)
std::size_t rx_abs_draw = 0; // rendered column for drawing std::size_t rx_abs_draw = 0; // rendered column for drawing
// Compute search highlight ranges for this line in source indices // Compute search highlight ranges for this line in source indices
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges; std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
if (search_mode) { if (search_mode) {
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring // 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)) { if (ed.PromptActive() && (
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try { try {
std::regex rx(ed.SearchQuery()); std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
@@ -297,28 +303,31 @@ GUIRenderer::Draw(Editor &ed)
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i; bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0; std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0; std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
for (const auto &rg : hl_src_ranges) { for (const auto &rg: hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second; std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx); std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex); std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset // Apply horizontal scroll offset
if (rx_end <= coloffs_now) continue; // fully left of view 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 vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now; std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y); 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); ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
// Choose color: current match stronger // Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end; 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); ImU32 col = is_current
? IM_COL32(255, 220, 120, 140)
: IM_COL32(200, 200, 0, 90);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
} }
} }
// Emit entire line (ImGui child scrolling will handle clipping) // Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) { for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src]; char c = line[src];
if (c == '\t') { if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw)); std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' '); expanded.append(adv, ' ');
rx_abs_draw += adv; rx_abs_draw += adv;
} else { } else {
@@ -327,7 +336,37 @@ GUIRenderer::Draw(Editor &ed)
} }
} }
// 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.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str()); ImGui::TextUnformatted(expanded.c_str());
}
// Draw a visible cursor indicator on the current line // Draw a visible cursor indicator on the current line
if (i == cy) { if (i == cy) {
@@ -390,13 +429,18 @@ GUIRenderer::Draw(Editor &ed)
float max_px = std::max(0.0f, right_x - left_x); float max_px = std::max(0.0f, right_x - left_x);
std::string prefix; std::string prefix;
if (!label.empty()) prefix = label + ": "; if (kind == Editor::PromptKind::Command) {
prefix = ": ";
} else if (!label.empty()) {
prefix = label + ": ";
}
// Compose showing right-end of filename portion when too long for space // Compose showing right-end of filename portion when too long for space
std::string final_msg; std::string final_msg;
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str()); ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
float avail_px = std::max(0.0f, max_px - prefix_sz.x); 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) { 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 // Trim from left until it fits by pixel width
std::string tail = ptext; std::string tail = ptext;
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str()); ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
@@ -408,7 +452,11 @@ GUIRenderer::Draw(Editor &ed)
while (start < tail.size()) { while (start < tail.size()) {
// Estimate how many chars to skip based on ratio // Estimate how many chars to skip based on ratio
float ratio = tail_sz.x / avail_px; 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; 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; start += skip;
std::string candidate = tail.substr(start); std::string candidate = tail.substr(start);
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str()); ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
@@ -425,7 +473,10 @@ GUIRenderer::Draw(Editor &ed)
while (lo < hi) { while (lo < hi) {
size_t mid = (lo + hi) / 2; size_t mid = (lo + hi) / 2;
std::string cand = tail.substr(mid); std::string cand = tail.substr(mid);
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1; if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
hi = mid;
else
lo = mid + 1;
} }
tail = tail.substr(lo); tail = tail.substr(lo);
} }
@@ -529,7 +580,8 @@ GUIRenderer::Draw(Editor &ed)
} }
// Draw right // Draw right
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f)); ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
p0.y + (bar_h - right_sz.y) * 0.5f));
ImGui::TextUnformatted(right.c_str()); ImGui::TextUnformatted(right.c_str());
// Draw middle message centered in remaining space // Draw middle message centered in remaining space

1053
GUITheme.h

File diff suppressed because it is too large Load Diff

48
GoHighlighter.cc Normal file
View File

@@ -0,0 +1,48 @@
#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
GoHighlighter.h Normal file
View 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

View File

@@ -15,24 +15,26 @@ HelpText::Text()
return std::string( return std::string(
"KTE - Kyle's Text Editor\n\n" "KTE - Kyle's Text Editor\n\n"
"About:\n" "About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n" " kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n" " WordStar/VDE-style command model with some emacs influences.\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
"\n" "\n"
"Core keybindings:\n" "K-commands (prefix C-k):\n"
" C-k ' Toggle read-only\n" " C-k ' Toggle read-only\n"
" C-k - Unindent region\n" " C-k - Unindent region (mark required)\n"
" C-k = Indent region\n" " C-k = Indent region (mark required)\n"
" C-k ; Command prompt (:\\ )\n"
" C-k C-d Kill entire line\n" " C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\n" " C-k C-q Quit now (no confirm)\n"
" C-k a Mark all and jump to end\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 b Switch buffer\n"
" C-k c Close current buffer\n" " C-k c Close current buffer\n"
" C-k d Kill to end of line\n" " C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n" " C-k e Open file (prompt)\n"
" C-k f Flush kill ring\n"
" C-k g Jump to line\n" " C-k g Jump to line\n"
" C-k h Show this help\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 l Reload buffer from disk\n"
" C-k n Previous buffer\n" " C-k n Previous buffer\n"
" C-k o Change working directory (prompt)\n" " C-k o Change working directory (prompt)\n"
@@ -44,12 +46,36 @@ HelpText::Text()
" C-k v Toggle visual file picker (GUI)\n" " C-k v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n" " C-k w Show working directory\n"
" C-k x Save and quit\n" " C-k x Save and quit\n"
" C-k y Yank\n"
"\n" "\n"
"ESC/Alt commands:\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" " ESC q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n" "\n"
" ESC d Delete next word\n" "Control keys:\n"
" Alt-w Copy region to kill ring\n\n" " C-a C-e Line start / end\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\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"
); );
} }

39
Highlight.h Normal file
View File

@@ -0,0 +1,39 @@
// 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

94
HighlighterEngine.cc Normal file
View File

@@ -0,0 +1,94 @@
#include "HighlighterEngine.h"
#include "Buffer.h"
#include "LanguageHighlighter.h"
namespace kte {
HighlighterEngine::HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine() = default;
void
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
{
hl_ = std::move(hl);
cache_.clear();
state_cache_.clear();
}
const LineHighlight &
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
{
auto it = cache_.find(row);
if (it != cache_.end()) {
if (it->second.version == buf_version) {
return it->second;
}
}
LineHighlight updated;
updated.version = buf_version;
updated.spans.clear();
if (!hl_) {
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
}
if (auto *stateful = dynamic_cast<StatefulHighlighter *>(hl_.get())) {
// Find nearest cached state at or before row-1 with matching version
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;
}
}
// Walk from start_row+1 up to row computing states; only collect spans at the target row
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? updated.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, prev_state, out);
// store state for this row (state after finishing r)
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
prev_state = next_state;
}
} else {
// Stateless path
hl_->HighlightLine(buf, row, updated.spans);
}
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
}
void
HighlighterEngine::InvalidateFrom(int row)
{
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;
}
}
}
} // namespace kte

45
HighlighterEngine.h Normal file
View File

@@ -0,0 +1,45 @@
// HighlighterEngine.h - caching layer for per-line highlights
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
#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_); }
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_;
};
} // namespace kte

93
HighlighterRegistry.cc Normal file
View File

@@ -0,0 +1,93 @@
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include <algorithm>
#include <filesystem>
// Forward declare simple highlighters implemented in this project
namespace kte {
class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
}
// 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"
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";
return f;
}
std::unique_ptr<LanguageHighlighter> HighlighterRegistry::CreateFor(std::string_view filetype)
{
std::string ft = Normalize(filetype);
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>();
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";
}
// Shebang
std::string ft = shebang_to_ft(first_line);
return ft;
}
} // namespace kte

26
HighlighterRegistry.h Normal file
View File

@@ -0,0 +1,26 @@
// HighlighterRegistry.h - create/detect language highlighters
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include "LanguageHighlighter.h"
namespace kte {
class HighlighterRegistry {
public:
// 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);
};
} // namespace kte

42
JsonHighlighter.cc Normal file
View File

@@ -0,0 +1,42 @@
#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

14
JsonHighlighter.h Normal file
View File

@@ -0,0 +1,14 @@
// 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

View File

@@ -108,6 +108,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case '=': case '=':
out = CommandId::IndentRegion; out = CommandId::IndentRegion;
return true; return true;
case ';':
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
return true;
default: default:
break; break;
} }

43
LanguageHighlighter.h Normal file
View File

@@ -0,0 +1,43 @@
// 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

41
LispHighlighter.cc Normal file
View File

@@ -0,0 +1,41 @@
#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
LispHighlighter.h Normal file
View 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

88
MarkdownHighlighter.cc Normal file
View File

@@ -0,0 +1,88 @@
#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
MarkdownHighlighter.h Normal file
View 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

16
NullHighlighter.cc Normal file
View File

@@ -0,0 +1,16 @@
#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

13
NullHighlighter.h Normal file
View File

@@ -0,0 +1,13 @@
// 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

85
PythonHighlighter.cc Normal file
View File

@@ -0,0 +1,85 @@
#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

18
PythonHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// 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

39
RustHighlighter.cc Normal file
View File

@@ -0,0 +1,39 @@
#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
RustHighlighter.h Normal file
View 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

43
ShellHighlighter.cc Normal file
View File

@@ -0,0 +1,43 @@
#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

13
ShellHighlighter.h Normal file
View File

@@ -0,0 +1,13 @@
// 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

View File

@@ -9,6 +9,7 @@
#include "TerminalRenderer.h" #include "TerminalRenderer.h"
#include "Buffer.h" #include "Buffer.h"
#include "Editor.h" #include "Editor.h"
#include "Highlight.h"
// Version string expected to be provided by build system as KTE_VERSION_STR // Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR #ifndef KTE_VERSION_STR
@@ -48,11 +49,13 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t src_i = 0; std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active // Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end) std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
if (search_mode && li < lines.size()) { if (search_mode && li < lines.size()) {
std::string sline = static_cast<std::string>(lines[li]); 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 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)) { if (ed.PromptActive() && (
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try { try {
std::regex rx(ed.SearchQuery()); std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx); for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
@@ -75,12 +78,15 @@ TerminalRenderer::Draw(Editor &ed)
} }
} }
auto is_src_in_hl = [&](std::size_t si) -> bool { auto is_src_in_hl = [&](std::size_t si) -> bool {
if (ranges.empty()) return false; if (ranges.empty())
return false;
// ranges are non-overlapping and ordered by construction // ranges are non-overlapping and ordered by construction
// linear scan is fine for now // linear scan is fine for now
for (const auto &rg : ranges) { for (const auto &rg: ranges) {
if (si < rg.first) break; if (si < rg.first)
if (si >= rg.first && si < rg.second) return true; break;
if (si >= rg.first && si < rg.second)
return true;
} }
return false; return false;
}; };
@@ -96,6 +102,42 @@ TerminalRenderer::Draw(Editor &ed)
std::string line = static_cast<std::string>(lines[li]); std::string line = static_cast<std::string>(lines[li]);
src_i = 0; src_i = 0;
render_col = 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) { while (written < cols) {
char ch = ' '; char ch = ' ';
bool from_src = false; bool from_src = false;
@@ -119,15 +161,35 @@ TerminalRenderer::Draw(Editor &ed)
// Now render visible spaces // Now render visible spaces
while (next_tab > 0 && written < cols) { while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i); 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; bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes // Toggle highlight attributes
int attr = 0; int attr = 0;
if (in_hl) attr |= A_STANDOUT; if (in_hl)
if (in_cur) attr |= A_BOLD; attr |= A_STANDOUT;
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; } if (in_cur)
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; } attr |= A_BOLD;
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; } if ((attr & A_STANDOUT) && !hl_on) {
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; } attron(A_STANDOUT);
hl_on = true;
}
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(' '); addch(' ');
++written; ++written;
++render_col; ++render_col;
@@ -151,11 +213,28 @@ TerminalRenderer::Draw(Editor &ed)
from_src = false; from_src = false;
} }
bool in_hl = search_mode && from_src && is_src_in_hl(src_i); 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; bool in_cur =
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; } has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; } cur_mend;
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; } if (in_hl && !hl_on) {
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; } 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)); addch(static_cast<unsigned char>(ch));
++written; ++written;
++render_col; ++render_col;
@@ -173,6 +252,7 @@ TerminalRenderer::Draw(Editor &ed)
attroff(A_BOLD); attroff(A_BOLD);
cur_on = false; cur_on = false;
} }
attrset(A_NORMAL);
clrtoeol(); clrtoeol();
} }
@@ -222,20 +302,23 @@ TerminalRenderer::Draw(Editor &ed)
} }
// Prefer keeping the tail of the filename visible when it exceeds the window // Prefer keeping the tail of the filename visible when it exceeds the window
std::string msg; std::string msg;
if (!label.empty()) { if (kind == Editor::PromptKind::Command) {
msg = ": ";
} else if (!label.empty()) {
msg = label + ": "; msg = label + ": ";
} }
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible // 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) { if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
Editor::PromptKind::Chdir) && cols > 0) {
int avail = cols - static_cast<int>(msg.size()); int avail = cols - static_cast<int>(msg.size());
if (avail <= 0) { if (avail <= 0) {
// No room for label; fall back to showing the rightmost portion of the whole string // No room for label; fall back to showing the rightmost portion of the whole string
std::string whole = msg + ptext; std::string whole = msg + ptext;
if ((int)whole.size() > cols) if ((int) whole.size() > cols)
whole = whole.substr(whole.size() - cols); whole = whole.substr(whole.size() - cols);
msg = whole; msg = whole;
} else { } else {
if ((int)ptext.size() > avail) { if ((int) ptext.size() > avail) {
ptext = ptext.substr(ptext.size() - avail); ptext = ptext.substr(ptext.size() - avail);
} }
msg += ptext; msg += ptext;

View File

@@ -338,31 +338,42 @@ UndoSystem::UpdateBufferReference(Buffer &new_buf)
buf_ = &new_buf; buf_ = &new_buf;
} }
// ---- Debug helpers ---- // ---- Debug helpers ----
const char * const char *
UndoSystem::type_str(UndoType t) UndoSystem::type_str(UndoType t)
{ {
switch (t) { switch (t) {
case UndoType::Insert: return "Insert"; case UndoType::Insert:
case UndoType::Delete: return "Delete"; return "Insert";
case UndoType::Paste: return "Paste"; case UndoType::Delete:
case UndoType::Newline: return "Newline"; return "Delete";
case UndoType::DeleteRow: return "DeleteRow"; case UndoType::Paste:
return "Paste";
case UndoType::Newline:
return "Newline";
case UndoType::DeleteRow:
return "DeleteRow";
} }
return "?"; return "?";
} }
bool bool
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target) UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
{ {
if (!root || !target) return false; if (!root || !target)
if (root == target) return true; return false;
if (root == target)
return true;
for (UndoNode *child = root->child; child != nullptr; child = child->next) { for (UndoNode *child = root->child; child != nullptr; child = child->next) {
if (is_descendant(child, target)) return true; if (is_descendant(child, target))
return true;
} }
return false; return false;
} }
void void
UndoSystem::debug_log(const char *op) const UndoSystem::debug_log(const char *op) const
{ {
@@ -374,14 +385,14 @@ UndoSystem::debug_log(const char *op) const
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n", "[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
op, op,
row, col, row, col,
(const void*)p, (const void *) p,
p ? type_str(p->type) : "-", p ? type_str(p->type) : "-",
p ? p->row : -1, p ? p->row : -1,
p ? p->col : -1, p ? p->col : -1,
p ? p->text.size() : 0, p ? p->text.size() : 0,
(void*)tree_.current, (void *) tree_.current,
(void*)tree_.saved); (void *) tree_.saved);
#else #else
(void)op; (void) op;
#endif #endif
} }

View File

@@ -43,7 +43,9 @@ private:
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined) // Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
void debug_log(const char *op) const; void debug_log(const char *op) const;
static const char *type_str(UndoType t); static const char *type_str(UndoType t);
static bool is_descendant(UndoNode *root, const UndoNode *target); static bool is_descendant(UndoNode *root, const UndoNode *target);
void update_dirty_flag(); void update_dirty_flag();

View File

@@ -1,7 +1,7 @@
.\" kge(1) — Kyle's Graphical Editor (GUI-first) .\" kge(1) — Kyle's Graphical Editor (GUI-first)
.\" .\"
.\" Project homepage: https://github.com/wntrmute/kte .\" 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 .SH NAME
kge \- Kyle's Graphical Editor (GUI-first) kge \- Kyle's Graphical Editor (GUI-first)
.SH SYNOPSIS .SH SYNOPSIS
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
.PP .PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G. Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP .TP
.B C-k BACKSPACE .B C-k '
Delete from the cursor to the beginning of the line. Toggle read-only for the current buffer.
.TP
.B C-k SPACE
Toggle the mark.
.TP .TP
.B C-k - .B C-k -
If the mark is set, unindent the region. If the mark is set, unindent the region.
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
.B C-k = .B C-k =
If the mark is set, indent the region. If the mark is set, indent the region.
.TP .TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a .B C-k a
Set the mark at the beginning of the file, then jump to the end of the file. Set the mark at the beginning of the file, then jump to the end of the file.
.TP .TP
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
Delete the entire line. Delete the entire line.
.TP .TP
.B C-k e .B C-k e
Edit a new file. Edit (open) a new file.
.TP .TP
.B C-k f .B C-k f
Flush the kill ring. Flush the kill ring.
@@ -88,14 +88,20 @@ Flush the kill ring.
.B C-k g .B C-k g
Go to a specific line. Go to a specific line.
.TP .TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.TP
.B C-k j .B C-k j
Jump to the mark. Jump to the mark.
.TP .TP
.B C-k l .B C-k l
Reload the current buffer from disk. Reload the current buffer from disk.
.TP .TP
.B C-k m .B C-k n
Run make(1), reporting success or failure. Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP .TP
.B C-k p .B C-k p
Switch to the next buffer. 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 .B C-k C-q
Immediately exit the editor. Immediately exit the editor.
.TP .TP
.B C-k r
Redo changes.
.TP
.B C-k s .B C-k s
Save the file, prompting for a filename if needed. Save the file, prompting for a filename if needed.
.TP .TP
.B C-k u .B C-k u
Undo. Undo.
.TP .TP
.B C-k r .B C-k v
Redo changes. Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP .TP
.B C-k x .B C-k x
Save the file and exit. Also C-k C-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 .B C-k y
Yank the kill ring. Yank the kill ring.
.TP .TP
.B C-k \e .B C-k C-x
Dump core. Save the file and exit.
.SS Other keybindings .SS Other keybindings
.TP .TP
.B C-g .B C-g
Cancel the current operation. Cancel the current operation.
.TP .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 .B C-l
Refresh the display. Refresh the display.
.TP .TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r .B C-r
Regex search. Regex search.
.TP .TP
.B C-s .B C-s
Incremental find. Incremental find.
.TP .TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u .B C-u
Universal argument. C-u followed by numbers will repeat an operation n times. Universal argument. C-u followed by numbers will repeat an operation n times.
.TP .TP
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
.B C-y .B C-y
Yank the kill ring. Yank the kill ring.
.TP .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 .B ESC BACKSPACE
Delete the previous word. Delete the previous word.
.TP .TP

View File

@@ -1,7 +1,7 @@
.\" kte(1) — Kyle's Text Editor (terminal-first) .\" kte(1) — Kyle's Text Editor (terminal-first)
.\" .\"
.\" Project homepage: https://github.com/wntrmute/kte .\" 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 .SH NAME
kte \- Kyle's Text Editor (terminal-first) kte \- Kyle's Text Editor (terminal-first)
.SH SYNOPSIS .SH SYNOPSIS
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
.PP .PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G. Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP .TP
.B C-k BACKSPACE .B C-k '
Delete from the cursor to the beginning of the line. Toggle read-only for the current buffer.
.TP
.B C-k SPACE
Toggle the mark.
.TP .TP
.B C-k - .B C-k -
If the mark is set, unindent the region. If the mark is set, unindent the region.
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
.B C-k = .B C-k =
If the mark is set, indent the region. If the mark is set, indent the region.
.TP .TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a .B C-k a
Set the mark at the beginning of the file, then jump to the end of the file. Set the mark at the beginning of the file, then jump to the end of the file.
.TP .TP
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
Delete the entire line. Delete the entire line.
.TP .TP
.B C-k e .B C-k e
Edit a new file. Edit (open) a new file.
.TP .TP
.B C-k f .B C-k f
Flush the kill ring. Flush the kill ring.
@@ -93,14 +93,20 @@ Flush the kill ring.
.B C-k g .B C-k g
Go to a specific line. Go to a specific line.
.TP .TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.TP
.B C-k j .B C-k j
Jump to the mark. Jump to the mark.
.TP .TP
.B C-k l .B C-k l
Reload the current buffer from disk. Reload the current buffer from disk.
.TP .TP
.B C-k m .B C-k n
Run make(1), reporting success or failure. Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP .TP
.B C-k p .B C-k p
Switch to the next buffer. 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 .B C-k C-q
Immediately exit the editor. Immediately exit the editor.
.TP .TP
.B C-k r
Redo changes.
.TP
.B C-k s .B C-k s
Save the file, prompting for a filename if needed. Save the file, prompting for a filename if needed.
.TP .TP
.B C-k u .B C-k u
Undo. Undo.
.TP .TP
.B C-k r .B C-k v
Redo changes. Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP .TP
.B C-k x .B C-k x
Save the file and exit. Also C-k C-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 .B C-k y
Yank the kill ring. Yank the kill ring.
.TP .TP
.B C-k \e .B C-k C-x
Dump core. 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 .SS Other keybindings
.TP .TP
.B C-g .B C-g
Cancel the current operation. Cancel the current operation.
.TP .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 .B C-l
Refresh the display. Refresh the display.
.TP .TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r .B C-r
Regex search. Regex search.
.TP .TP
.B C-s .B C-s
Incremental find. Incremental find.
.TP .TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u .B C-u
Universal argument. C-u followed by numbers will repeat an operation n times. Universal argument. C-u followed by numbers will repeat an operation n times.
.TP .TP
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
.B C-y .B C-y
Yank the kill ring. Yank the kill ring.
.TP .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 .B ESC BACKSPACE
Delete the previous word. Delete the previous word.
.TP .TP

102
docs/syntax on.md Normal file
View File

@@ -0,0 +1,102 @@
### Objective
Introduce fast, minimaldependency 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 perline highlighting; handle singleline comments and strings; defer multiline state to v2.
- Toggle: `:syntax on|off` and perbuffer 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; };` // 0based 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:
- PerBuffer 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.
- Zorder 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 semitransparent 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/16color 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>` — perbuffer 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 handrolled 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 perrow 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
- Viewportfirst highlighting: compute only visible rows each frame; background task warms cache around viewport.
- Reuse span buffers, avoid allocations; smallvector 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 Treesitter 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: featuredetect 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: perbuffer 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.

52
docs/syntax.md Normal file
View File

@@ -0,0 +1,52 @@
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 lines 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.

View File

@@ -1,3 +1,9 @@
#!/usr/bin/env bash #!/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}