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.
This commit is contained in:
2025-12-01 18:20:36 -08:00
parent 4d84b352eb
commit 1a77f28ce4
38 changed files with 1701 additions and 590 deletions

390
.idea/workspace.xml generated
View File

@@ -1,390 +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/LiveTemplatesHousekeeping/HotspotSessionHintIsShown/@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="bump version">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUITheme.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUITheme.h" 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" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
</component>
<component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="CurrentFile" />
</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">{
&quot;keyToString&quot;: {
&quot;CMake Application.kge.executor&quot;: &quot;Run&quot;,
&quot;CMake Application.test_example.executor&quot;: &quot;Run&quot;,
&quot;CMake Application.test_undo.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_PATH&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_PROFILES&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;&quot;,
&quot;RunOnceActivity.RadMigrateCodeStyle&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,
&quot;RunOnceActivity.west.config.association.type.startup.service&quot;: &quot;true&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;code.cleanup.on.save&quot;: &quot;true&quot;,
&quot;com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;junie.onboarding.icon.badge.shown&quot;: &quot;true&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;onboarding.tips.debug.path&quot;: &quot;/Users/kyle/src/kte/main.cpp&quot;,
&quot;rearrange.code.on.save&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;CMakeSettings&quot;,
&quot;to.speed.mode.migration.done&quot;: &quot;true&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</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="50201000" />
</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>
<task id="LOCAL-00020" summary="Fix double scroll issue.">
<option name="closed" value="true" />
<created>1764635874362</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1764635874362</updated>
</task>
<task id="LOCAL-00021" summary="bump version">
<option name="closed" value="true" />
<created>1764635891710</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1764635891710</updated>
</task>
<option name="localTasksCounter" value="22" />
<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" />
<MESSAGE value="Fix double scroll issue." />
<MESSAGE value="bump version" />
<option name="LAST_COMMIT_MESSAGE" value="bump version" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

View File

@@ -12,6 +12,10 @@
#include "AppendBuffer.h"
#include "UndoSystem.h"
#include <cstdint>
#include <memory>
#include "HighlighterEngine.h"
#include "Highlight.h"
class Buffer {
@@ -326,6 +330,12 @@ public:
void SetDirty(bool d)
{
dirty_ = d;
if (d) {
++version_;
if (highlighter_) {
highlighter_->InvalidateFrom(0);
}
}
}
@@ -364,6 +374,23 @@ public:
[[nodiscard]] std::string AsString() const;
// Syntax highlighting integration (per-buffer)
[[nodiscard]] std::uint64_t Version() const { return version_; }
void SetSyntaxEnabled(bool on) { syntax_enabled_ = on; }
[[nodiscard]] bool SyntaxEnabled() const { return syntax_enabled_; }
void SetFiletype(const std::string &ft) { filetype_ = ft; }
[[nodiscard]] const std::string &Filetype() const { return filetype_; }
kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
void EnsureHighlighter()
{
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>();
}
// Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text);
@@ -400,6 +427,12 @@ private:
// Per-buffer undo state
std::unique_ptr<struct UndoTree> undo_tree_;
std::unique_ptr<UndoSystem> undo_sys_;
// Syntax/highlighting state
std::uint64_t version_ = 0; // increment on edits
bool syntax_enabled_ = true;
std::string filetype_;
std::unique_ptr<kte::HighlighterEngine> highlighter_;
};
#endif // KTE_BUFFER_H

View File

@@ -67,6 +67,17 @@ set(COMMON_SOURCES
UndoNode.cc
UndoTree.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
@@ -90,6 +101,19 @@ set(COMMON_HEADERS
UndoNode.h
UndoTree.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

View File

@@ -7,10 +7,15 @@
#include <cctype>
#include "Command.h"
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#include "Editor.h"
#include "Buffer.h"
#include "UndoSystem.h"
#include "HelpText.h"
#include "LanguageHighlighter.h"
#include "HighlighterEngine.h"
#include "CppHighlighter.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#endif
@@ -757,6 +762,124 @@ cmd_unknown_kcommand(CommandContext &ctx)
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
@@ -3611,9 +3734,12 @@ InstallDefaultCommands()
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
cmd_change_working_directory_start
});
// UI helpers
CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
// UI helpers
CommandRegistry::Register(
{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

@@ -94,7 +94,10 @@ enum class CommandId {
// Theme by name
ThemeSetByName,
// Background mode (GUI)
BackgroundSet,
BackgroundSet,
// Syntax highlighting
Syntax, // ":syntax on|off|reload"
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
};

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 "Editor.h"
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include "NullHighlighter.h"
Editor::Editor() = default;
@@ -143,26 +146,72 @@ Editor::OpenFile(const std::string &path, std::string &err)
{
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
// of creating a new one.
if (buffers_.size() == 1) {
Buffer &cur = buffers_[curbuf_];
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
const bool clean = !cur.Dirty();
const auto &rows = cur.Rows();
const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) {
return cur.OpenFromFile(path, err);
}
}
if (buffers_.size() == 1) {
Buffer &cur = buffers_[curbuf_];
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
const bool clean = !cur.Dirty();
const auto &rows = cur.Rows();
const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) {
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;
}
}
Buffer b;
if (!b.OpenFromFile(path, err)) {
return false;
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
return true;
Buffer b;
if (!b.OpenFromFile(path, err)) {
return false;
}
// Initialize syntax highlighting by extension + shebang via registry (v2)
b.EnsureHighlighter();
std::string first = "";
{
const auto &rows = b.Rows();
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
}
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
b.SetFiletype(ft);
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
b.SetFiletype("");
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
return true;
}

View File

@@ -102,17 +102,27 @@ GUIConfig::LoadFromFile(const std::string &path)
if (v > 0.0f) {
font_size = v;
}
} else if (key == "theme") {
theme = val;
} else if (key == "background" || key == "bg") {
std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (v == "light" || v == "dark")
background = v;
}
}
} else if (key == "theme") {
theme = val;
} else if (key == "background" || key == "bg") {
std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (v == "light" || v == "dark")
background = v;
} else if (key == "syntax") {
std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (v == "1" || v == "on" || v == "true" || v == "yes") {
syntax = true;
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
syntax = false;
}
}
}
return true;
return true;
}

View File

@@ -12,14 +12,18 @@
class GUIConfig {
public:
bool fullscreen = false;
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
std::string theme = "nord";
// Background mode for themes that support light/dark variants
// Values: "dark" (default), "light"
std::string background = "dark";
bool fullscreen = false;
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
std::string theme = "nord";
// Background mode for themes that support light/dark variants
// Values: "dark" (default), "light"
std::string background = "dark";
// Default syntax highlighting state for GUI (kge): on/off
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
bool syntax = true; // default: enabled
// Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load();

View File

@@ -16,6 +16,8 @@
#include "Font.h" // embedded default font (DefaultFontRegular)
#include "GUIConfig.h"
#include "GUITheme.h"
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#ifndef KTE_FONT_SIZE
@@ -106,12 +108,42 @@ GUIFrontend::Init(Editor &ed)
(void) io;
ImGui::StyleColorsDark();
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
// Apply default syntax highlighting preference from GUI config to the current buffer
if (Buffer *b = ed.CurrentBuffer()) {
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
// Ensure a highlighter is available if possible
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
// Try detect from filename and first line; fall back to cpp or existing filetype
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty()) first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false;

View File

@@ -10,6 +10,8 @@
#include <regex>
#include "GUIRenderer.h"
#include "Highlight.h"
#include "GUITheme.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
@@ -321,21 +323,50 @@ GUIRenderer::Draw(Editor &ed)
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line (ImGui child scrolling will handle clipping)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
ImGui::TextUnformatted(expanded.c_str());
// Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version());
// Helper to convert a src column to expanded rx position
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
for (const auto &sp: lh.spans) {
std::size_t rx_s = src_to_rx_full(static_cast<std::size_t>(std::max(0, sp.col_start)));
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
if (rx_e <= coloffs_now)
continue;
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
if (vx0 >= expanded.size()) continue;
vx1 = std::min<std::size_t>(vx1, expanded.size());
if (vx1 <= vx0) continue;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
}
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str());
}
// Draw a visible cursor indicator on the current line
if (i == cy) {

View File

@@ -1,57 +1,48 @@
// GUITheme.h - ImGui theme configuration for kte GUI
// Provides theme palettes and style settings (Nord + Gruvbox variants).
// GUITheme.h ImGui theming helpers and background mode
#pragma once
#include <imgui.h>
#include <algorithm>
#include <memory>
#include <vector>
#include <memory>
#include <string>
#include <cstddef>
#include <algorithm>
#include <cctype>
namespace kte {
// Theme identifiers (legacy API kept for compatibility)
enum class ThemeId {
Nord,
GruvboxDarkMedium,
GruvboxLightMedium,
EInk, // monochrome e-ink style
Solarized, // solarized (light/dark via background)
Plan9, // plan9-inspired minimal theme (single acme-like palette)
};
// Background mode for themes that support light/dark variants
enum class BackgroundMode {
Dark,
Light,
};
// Forward declaration of registry helpers
class Theme;
static inline const std::vector<std::unique_ptr<Theme> > &ThemeRegistry();
static inline size_t ThemeIndexFromId(ThemeId id);
static inline ThemeId ThemeIdFromIndex(size_t idx);
// Keep track of current theme (program-wide)
static inline ThemeId gCurrentTheme = ThemeId::Nord;
// Background preference (defaults to Dark)
static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
// Mirror index of current theme in the registry; keep it consistent with gCurrentTheme
// Current alphabetical order: 0=eink, 1=gruvbox, 2=nord, 3=solarized
static inline size_t gCurrentThemeIndex = ThemeIndexFromId(gCurrentTheme);
// Convert RGB hex (0xRRGGBB) to ImVec4 with optional alpha
static inline ImVec4
RGBA(unsigned int rgb, float a = 1.0f)
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
static inline ImVec4 RGBA(unsigned int rgb, float a = 1.0f)
{
float r = ((rgb >> 16) & 0xFF) / 255.0f;
float g = ((rgb >> 8) & 0xFF) / 255.0f;
float b = ((rgb) & 0xFF) / 255.0f;
return ImVec4(r, g, b, a);
const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f;
const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
return ImVec4(r, g, b, a);
}
namespace kte {
// Background mode selection for light/dark palettes
enum class BackgroundMode { Light, Dark };
// Global background mode; default to Dark to match prior defaults
static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
// Basic theme identifier (kept minimal; some ids are aliases)
enum class ThemeId {
EInk = 0,
GruvboxDarkMedium = 1,
GruvboxLightMedium = 1, // alias to unified gruvbox index
Nord = 2,
Plan9 = 3,
Solarized = 4,
};
// Current theme tracking
static inline ThemeId gCurrentTheme = ThemeId::Nord;
static inline std::size_t gCurrentThemeIndex = 0;
// Forward declarations for helpers used below
static inline size_t ThemeIndexFromId(ThemeId id);
static inline ThemeId ThemeIdFromIndex(size_t idx);
// Helpers to set/query background mode
static inline void
@@ -1107,20 +1098,18 @@ CurrentThemeName()
static inline size_t
ThemeIndexFromId(ThemeId id)
{
switch (id) {
case ThemeId::EInk:
return 0;
case ThemeId::GruvboxDarkMedium:
return 1;
case ThemeId::GruvboxLightMedium: // legacy alias maps to unified gruvbox index
return 1;
case ThemeId::Nord:
return 2;
case ThemeId::Plan9:
return 3;
case ThemeId::Solarized:
return 4;
}
switch (id) {
case ThemeId::EInk:
return 0;
case ThemeId::GruvboxDarkMedium:
return 1;
case ThemeId::Nord:
return 2;
case ThemeId::Plan9:
return 3;
case ThemeId::Solarized:
return 4;
}
return 0;
}
@@ -1142,4 +1131,30 @@ ThemeIdFromIndex(size_t idx)
return ThemeId::Solarized;
}
}
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
static inline ImVec4 SyntaxInk(TokenKind k)
{
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
// Base text
ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
switch (k) {
case TokenKind::Keyword: return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
case TokenKind::Type: return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
case TokenKind::String: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Char: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Comment: return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
case TokenKind::Number: return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
case TokenKind::Preproc: return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
case TokenKind::Constant: return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
case TokenKind::Function: return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
case TokenKind::Operator: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Punctuation: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Identifier: return def;
case TokenKind::Whitespace: return def;
case TokenKind::Error: return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
case TokenKind::Default: default: return def;
}
}
} // namespace kte

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

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

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 "Buffer.h"
#include "Editor.h"
#include "Highlight.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
@@ -97,13 +98,49 @@ TerminalRenderer::Draw(Editor &ed)
bool hl_on = false;
bool cur_on = false;
int written = 0;
if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
// Syntax highlighting: fetch per-line spans
const kte::LineHighlight *lh_ptr = nullptr;
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version());
}
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
if (!lh_ptr) return kte::TokenKind::Default;
for (const auto &sp: lh_ptr->spans) {
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end)
return sp.kind;
}
return kte::TokenKind::Default;
};
auto apply_token_attr = [&](kte::TokenKind k) {
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
attrset(A_NORMAL);
switch (k) {
case kte::TokenKind::Keyword:
case kte::TokenKind::Type:
case kte::TokenKind::Constant:
case kte::TokenKind::Function:
attron(A_BOLD);
break;
case kte::TokenKind::Comment:
attron(A_DIM);
break;
case kte::TokenKind::String:
case kte::TokenKind::Char:
case kte::TokenKind::Number:
// standout a bit using A_UNDERLINE if available
attron(A_UNDERLINE);
break;
default:
break;
}
};
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') {
@@ -122,41 +159,45 @@ TerminalRenderer::Draw(Editor &ed)
next_tab -= to_skip;
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl)
attr |= A_STANDOUT;
if (in_cur)
attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!(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;
}
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl)
attr |= A_STANDOUT;
if (in_cur)
attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
// Apply syntax attribute only if not in search highlight
if (!in_hl) {
apply_token_attr(token_at(src_i));
}
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
// normal char
if (render_col < coloffs) {
++render_col;
@@ -171,45 +212,49 @@ TerminalRenderer::Draw(Editor &ed)
ch = ' ';
from_src = false;
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
cur_mend;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++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;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
if (!in_hl && from_src) {
apply_token_attr(token_at(src_i));
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
clrtoeol();
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
attrset(A_NORMAL);
clrtoeol();
}
// Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury();

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
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}