Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bfe75fbf0 | |||
| d15b241140 | |||
| ceef6af3ae | |||
| e62cf3ee28 | |||
| 1a77f28ce4 | |||
| 4d84b352eb | |||
| 1892075d82 | |||
| 719862c842 | |||
| 655cc40162 |
369
.idea/workspace.xml
generated
369
.idea/workspace.xml
generated
@@ -1,369 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="BackendCodeEditorMiscSettings">
|
|
||||||
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
|
|
||||||
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
|
|
||||||
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakePresetLoader">{
|
|
||||||
"useNewFormat": true
|
|
||||||
}</component>
|
|
||||||
<component name="CMakeProjectFlavorService">
|
|
||||||
<option name="flavorId" value="CMakePlainProjectFlavor" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakeReloadState">
|
|
||||||
<option name="reloaded" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakeRunConfigurationManager">
|
|
||||||
<generated>
|
|
||||||
<config projectName="kte" targetName="kte" />
|
|
||||||
<config projectName="kte" targetName="imgui" />
|
|
||||||
<config projectName="kte" targetName="kge" />
|
|
||||||
</generated>
|
|
||||||
</component>
|
|
||||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
|
||||||
<configurations>
|
|
||||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON -DBUILD_GUI:BOOL=ON" />
|
|
||||||
</configurations>
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real">
|
|
||||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="ClangdSettings">
|
|
||||||
<option name="formatViaClangd" value="false" />
|
|
||||||
</component>
|
|
||||||
<component name="ExecutionTargetManager" SELECTED_TARGET="CMakeBuildProfile:Debug" />
|
|
||||||
<component name="FormatOnSaveOptions">
|
|
||||||
<option name="myRunOnSave" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="PUSH_TAGS">
|
|
||||||
<GitPushTagMode>
|
|
||||||
<option name="argument" value="--tags" />
|
|
||||||
<option name="title" value="All" />
|
|
||||||
</GitPushTagMode>
|
|
||||||
</option>
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
<option name="UPDATE_TYPE" value="REBASE" />
|
|
||||||
</component>
|
|
||||||
<component name="HighlightingSettingsPerFile">
|
|
||||||
<setting file="mock:///AIAssistantSnippet." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
</component>
|
|
||||||
<component name="OptimizeOnSaveOptions">
|
|
||||||
<option name="myRunOnSave" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="ProblemsViewState">
|
|
||||||
<option name="selectedTabId" value="AISelfReview" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectApplicationVersion">
|
|
||||||
<option name="ide" value="CLion" />
|
|
||||||
<option name="majorVersion" value="2025" />
|
|
||||||
<option name="minorVersion" value="2.5" />
|
|
||||||
<option name="productBranch" value="Classic" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo">{
|
|
||||||
"associatedIndex": 3
|
|
||||||
}</component>
|
|
||||||
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
|
|
||||||
<component name="ProjectLevelVcsManager">
|
|
||||||
<OptionsSetting value="false" id="Update" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
<option name="sortByType" value="true" />
|
|
||||||
<option name="sortKey" value="BY_TYPE" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
|
||||||
"keyToString": {
|
|
||||||
"CMake Application.kge.executor": "Run",
|
|
||||||
"CMake Application.test_example.executor": "Run",
|
|
||||||
"CMake Application.test_undo.executor": "Run",
|
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
|
||||||
"NIXITCH_NIX_OTHER_STORES": "",
|
|
||||||
"NIXITCH_NIX_PATH": "",
|
|
||||||
"NIXITCH_NIX_PROFILES": "",
|
|
||||||
"NIXITCH_NIX_REMOTE": "",
|
|
||||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
|
||||||
"RunOnceActivity.RadMigrateCodeStyle": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
|
||||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
|
||||||
"RunOnceActivity.west.config.association.type.startup.service": "true",
|
|
||||||
"cf.first.check.clang-format": "false",
|
|
||||||
"cidr.known.project.marker": "true",
|
|
||||||
"code.cleanup.on.save": "true",
|
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
|
||||||
"git-widget-placeholder": "master",
|
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
|
||||||
"node.js.detected.package.eslint": "true",
|
|
||||||
"node.js.detected.package.tslint": "true",
|
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
|
||||||
"nodejs_package_manager_path": "npm",
|
|
||||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
|
||||||
"rearrange.code.on.save": "true",
|
|
||||||
"settings.editor.selected.configurable": "CMakeSettings",
|
|
||||||
"to.speed.mode.migration.done": "true",
|
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
|
||||||
}]]></component>
|
|
||||||
<component name="RecentsManager">
|
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
|
||||||
<recent name="$PROJECT_DIR$/docs" />
|
|
||||||
</key>
|
|
||||||
</component>
|
|
||||||
<component name="RunManager" selected="CMake Application.kge">
|
|
||||||
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
|
||||||
<method v="2">
|
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
|
|
||||||
<method v="2">
|
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
|
|
||||||
<method v="2">
|
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
|
|
||||||
<method v="2">
|
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
<list>
|
|
||||||
<item itemvalue="CMake Application.imgui" />
|
|
||||||
<item itemvalue="CMake Application.kge" />
|
|
||||||
<item itemvalue="CMake Application.kte" />
|
|
||||||
</list>
|
|
||||||
</component>
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="" />
|
|
||||||
<created>1764457173148</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1764457173148</updated>
|
|
||||||
<workItem from="1764457174208" duration="46950000" />
|
|
||||||
<workItem from="1764538560497" duration="215000" />
|
|
||||||
<workItem from="1764539255906" duration="196000" />
|
|
||||||
<workItem from="1764539459951" duration="64000" />
|
|
||||||
<workItem from="1764539535105" duration="10000" />
|
|
||||||
<workItem from="1764539556448" duration="156000" />
|
|
||||||
<workItem from="1764539725338" duration="1075000" />
|
|
||||||
<workItem from="1764542392763" duration="3512000" />
|
|
||||||
<workItem from="1764548345516" duration="39312000" />
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764485311566</created>
|
|
||||||
<option name="number" value="00001" />
|
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764485311566</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00002" summary="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764486011231</created>
|
|
||||||
<option name="number" value="00002" />
|
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764486011231</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00003" summary="Handle end-of-file newline semantics and improve scroll alignment logic.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764486876984</created>
|
|
||||||
<option name="number" value="00003" />
|
|
||||||
<option name="presentableId" value="LOCAL-00003" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764486876984</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00004" summary="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764489870957</created>
|
|
||||||
<option name="number" value="00004" />
|
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764489870957</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00005" summary="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764496151303</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764496151303</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00006" summary="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764500200942</created>
|
|
||||||
<option name="number" value="00006" />
|
|
||||||
<option name="presentableId" value="LOCAL-00006" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764500200942</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00007" summary="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764501532446</created>
|
|
||||||
<option name="number" value="00007" />
|
|
||||||
<option name="presentableId" value="LOCAL-00007" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764501532446</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764502480274</created>
|
|
||||||
<option name="number" value="00008" />
|
|
||||||
<option name="presentableId" value="LOCAL-00008" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764502480274</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00009" summary="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764505723411</created>
|
|
||||||
<option name="number" value="00009" />
|
|
||||||
<option name="presentableId" value="LOCAL-00009" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764505723411</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00010" summary="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764550164829</created>
|
|
||||||
<option name="number" value="00010" />
|
|
||||||
<option name="presentableId" value="LOCAL-00010" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764550164829</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00011" summary="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764551986561</created>
|
|
||||||
<option name="number" value="00011" />
|
|
||||||
<option name="presentableId" value="LOCAL-00011" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764551986561</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00012" summary="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764556512864</created>
|
|
||||||
<option name="number" value="00012" />
|
|
||||||
<option name="presentableId" value="LOCAL-00012" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764556512864</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00013" summary="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764556854788</created>
|
|
||||||
<option name="number" value="00013" />
|
|
||||||
<option name="presentableId" value="LOCAL-00013" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764556854788</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00014" summary="Actually add the screenshot.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764557759844</created>
|
|
||||||
<option name="number" value="00014" />
|
|
||||||
<option name="presentableId" value="LOCAL-00014" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764557759844</updated>
|
|
||||||
</task>
|
|
||||||
<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 - Improve text input/event batching - Enhance debugging with optional instrumentation - Begin implementation of non-linear undo tree structure.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764619193516</created>
|
|
||||||
<option name="number" value="00018" />
|
|
||||||
<option name="presentableId" value="LOCAL-00018" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764619193517</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00019" summary="Add Nord theme for real">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764619210817</created>
|
|
||||||
<option name="number" value="00019" />
|
|
||||||
<option name="presentableId" value="LOCAL-00019" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764619210817</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="20" />
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
<component name="VCPKGProject">
|
|
||||||
<isAutomaticCheckingOnLaunch value="false" />
|
|
||||||
<isAutomaticFoundErrors value="true" />
|
|
||||||
<isAutomaticReloadCMake value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="VcsManagerConfiguration">
|
|
||||||
<MESSAGE value="Refactoring" />
|
|
||||||
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
|
|
||||||
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
|
||||||
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
|
||||||
<MESSAGE value="Enable installation targets." />
|
|
||||||
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
|
||||||
<MESSAGE value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
|
|
||||||
<MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
|
|
||||||
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`." />
|
|
||||||
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built." />
|
|
||||||
<MESSAGE value="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling." />
|
|
||||||
<MESSAGE value="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines." />
|
|
||||||
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code." />
|
|
||||||
<MESSAGE value="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup." />
|
|
||||||
<MESSAGE value="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0." />
|
|
||||||
<MESSAGE value="Actually add the screenshot." />
|
|
||||||
<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 - Improve text input/event batching - Enhance debugging with optional instrumentation - Begin implementation of non-linear undo tree structure." />
|
|
||||||
<MESSAGE value="Add Nord theme for real" />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Add Nord theme for real" />
|
|
||||||
</component>
|
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
|
||||||
<expand />
|
|
||||||
<select />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
56
Buffer.cc
56
Buffer.cc
@@ -6,6 +6,9 @@
|
|||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include "UndoTree.h"
|
#include "UndoTree.h"
|
||||||
|
// For reconstructing highlighter state on copies
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
Buffer::Buffer()
|
Buffer::Buffer()
|
||||||
@@ -40,9 +43,32 @@ Buffer::Buffer(const Buffer &other)
|
|||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
|
// Copy syntax/highlighting flags
|
||||||
|
version_ = other.version_;
|
||||||
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
filetype_ = other.filetype_;
|
||||||
// Fresh undo system for the copy
|
// Fresh undo system for the copy
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||||
|
|
||||||
|
// Recreate a highlighter engine for this copy based on filetype/syntax state
|
||||||
|
if (syntax_enabled_) {
|
||||||
|
// Allocate engine and install an appropriate highlighter
|
||||||
|
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||||
|
if (!filetype_.empty()) {
|
||||||
|
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
|
||||||
|
if (hl) {
|
||||||
|
highlighter_->SetHighlighter(std::move(hl));
|
||||||
|
} else {
|
||||||
|
// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
|
||||||
|
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No filetype -> keep syntax enabled but use NullHighlighter
|
||||||
|
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
// Fresh engine has empty caches; nothing to invalidate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -65,9 +91,28 @@ Buffer::operator=(const Buffer &other)
|
|||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
|
version_ = other.version_;
|
||||||
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
filetype_ = other.filetype_;
|
||||||
// Recreate undo system for this instance
|
// Recreate undo system for this instance
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
|
||||||
|
|
||||||
|
// Recreate highlighter engine consistent with syntax settings
|
||||||
|
highlighter_.reset();
|
||||||
|
if (syntax_enabled_) {
|
||||||
|
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||||
|
if (!filetype_.empty()) {
|
||||||
|
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
|
||||||
|
if (hl) {
|
||||||
|
highlighter_->SetHighlighter(std::move(hl));
|
||||||
|
} else {
|
||||||
|
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +136,11 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
undo_tree_(std::move(other.undo_tree_)),
|
undo_tree_(std::move(other.undo_tree_)),
|
||||||
undo_sys_(std::move(other.undo_sys_))
|
undo_sys_(std::move(other.undo_sys_))
|
||||||
{
|
{
|
||||||
|
// Move syntax/highlighting state
|
||||||
|
version_ = other.version_;
|
||||||
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
filetype_ = std::move(other.filetype_);
|
||||||
|
highlighter_ = std::move(other.highlighter_);
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
@@ -122,6 +172,12 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
undo_tree_ = std::move(other.undo_tree_);
|
undo_tree_ = std::move(other.undo_tree_);
|
||||||
undo_sys_ = std::move(other.undo_sys_);
|
undo_sys_ = std::move(other.undo_sys_);
|
||||||
|
|
||||||
|
// Move syntax/highlighting state
|
||||||
|
version_ = other.version_;
|
||||||
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
filetype_ = std::move(other.filetype_);
|
||||||
|
highlighter_ = std::move(other.highlighter_);
|
||||||
|
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
|
|||||||
70
Buffer.h
70
Buffer.h
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
#include "AppendBuffer.h"
|
#include "AppendBuffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include "syntax/HighlighterEngine.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
|
||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
@@ -262,6 +266,7 @@ public:
|
|||||||
return filename_;
|
return filename_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
||||||
// This does not mark the buffer as file-backed.
|
// This does not mark the buffer as file-backed.
|
||||||
void SetVirtualName(const std::string &name)
|
void SetVirtualName(const std::string &name)
|
||||||
@@ -282,17 +287,20 @@ public:
|
|||||||
return dirty_;
|
return dirty_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Read-only flag
|
// Read-only flag
|
||||||
[[nodiscard]] bool IsReadOnly() const
|
[[nodiscard]] bool IsReadOnly() const
|
||||||
{
|
{
|
||||||
return read_only_;
|
return read_only_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetReadOnly(bool ro)
|
void SetReadOnly(bool ro)
|
||||||
{
|
{
|
||||||
read_only_ = ro;
|
read_only_ = ro;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ToggleReadOnly()
|
void ToggleReadOnly()
|
||||||
{
|
{
|
||||||
read_only_ = !read_only_;
|
read_only_ = !read_only_;
|
||||||
@@ -322,6 +330,12 @@ public:
|
|||||||
void SetDirty(bool d)
|
void SetDirty(bool d)
|
||||||
{
|
{
|
||||||
dirty_ = d;
|
dirty_ = d;
|
||||||
|
if (d) {
|
||||||
|
++version_;
|
||||||
|
if (highlighter_) {
|
||||||
|
highlighter_->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -360,6 +374,56 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::string AsString() const;
|
[[nodiscard]] std::string AsString() const;
|
||||||
|
|
||||||
|
// Syntax highlighting integration (per-buffer)
|
||||||
|
[[nodiscard]] std::uint64_t Version() const
|
||||||
|
{
|
||||||
|
return version_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetSyntaxEnabled(bool on)
|
||||||
|
{
|
||||||
|
syntax_enabled_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool SyntaxEnabled() const
|
||||||
|
{
|
||||||
|
return syntax_enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetFiletype(const std::string &ft)
|
||||||
|
{
|
||||||
|
filetype_ = ft;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &Filetype() const
|
||||||
|
{
|
||||||
|
return filetype_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kte::HighlighterEngine *Highlighter()
|
||||||
|
{
|
||||||
|
return highlighter_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const kte::HighlighterEngine *Highlighter() const
|
||||||
|
{
|
||||||
|
return highlighter_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void EnsureHighlighter()
|
||||||
|
{
|
||||||
|
if (!highlighter_)
|
||||||
|
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||||
void insert_text(int row, int col, std::string_view text);
|
void insert_text(int row, int col, std::string_view text);
|
||||||
@@ -396,6 +460,12 @@ private:
|
|||||||
// Per-buffer undo state
|
// Per-buffer undo state
|
||||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||||
std::unique_ptr<UndoSystem> undo_sys_;
|
std::unique_ptr<UndoSystem> undo_sys_;
|
||||||
|
|
||||||
|
// Syntax/highlighting state
|
||||||
|
std::uint64_t version_ = 0; // increment on edits
|
||||||
|
bool syntax_enabled_ = true;
|
||||||
|
std::string filetype_;
|
||||||
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_BUFFER_H
|
#endif // KTE_BUFFER_H
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(KTE_VERSION "1.1.0")
|
set(KTE_VERSION "1.2.1")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
@@ -13,6 +13,7 @@ set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
|||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
||||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||||
|
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||||
|
|
||||||
if (CMAKE_HOST_UNIX)
|
if (CMAKE_HOST_UNIX)
|
||||||
message(STATUS "Build system is POSIX.")
|
message(STATUS "Build system is POSIX.")
|
||||||
@@ -37,6 +38,9 @@ else ()
|
|||||||
endif ()
|
endif ()
|
||||||
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
|
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
|
||||||
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
|
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
add_compile_definitions(KTE_ENABLE_TREESITTER)
|
||||||
|
endif ()
|
||||||
|
|
||||||
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
|
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
|
||||||
|
|
||||||
@@ -50,6 +54,29 @@ set(CURSES_NEED_WIDE)
|
|||||||
find_package(Curses REQUIRED)
|
find_package(Curses REQUIRED)
|
||||||
include_directories(${CURSES_INCLUDE_DIR})
|
include_directories(${CURSES_INCLUDE_DIR})
|
||||||
|
|
||||||
|
set(SYNTAX_SOURCES
|
||||||
|
syntax/GoHighlighter.cc
|
||||||
|
syntax/CppHighlighter.cc
|
||||||
|
syntax/JsonHighlighter.cc
|
||||||
|
syntax/ErlangHighlighter.cc
|
||||||
|
syntax/MarkdownHighlighter.cc
|
||||||
|
syntax/TreeSitterHighlighter.cc
|
||||||
|
syntax/LispHighlighter.cc
|
||||||
|
syntax/HighlighterEngine.cc
|
||||||
|
syntax/RustHighlighter.cc
|
||||||
|
syntax/HighlighterRegistry.cc
|
||||||
|
syntax/SqlHighlighter.cc
|
||||||
|
syntax/NullHighlighter.cc
|
||||||
|
syntax/ForthHighlighter.cc
|
||||||
|
syntax/PythonHighlighter.cc
|
||||||
|
syntax/ShellHighlighter.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
list(APPEND SYNTAX_SOURCES
|
||||||
|
TreeSitterHighlighter.cc)
|
||||||
|
endif ()
|
||||||
|
|
||||||
set(COMMON_SOURCES
|
set(COMMON_SOURCES
|
||||||
GapBuffer.cc
|
GapBuffer.cc
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
@@ -67,6 +94,41 @@ set(COMMON_SOURCES
|
|||||||
UndoNode.cc
|
UndoNode.cc
|
||||||
UndoTree.cc
|
UndoTree.cc
|
||||||
UndoSystem.cc
|
UndoSystem.cc
|
||||||
|
|
||||||
|
${SYNTAX_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
set(SYNTAX_HEADERS
|
||||||
|
syntax/GoHighlighter.h
|
||||||
|
syntax/HighlighterEngine.h
|
||||||
|
syntax/ShellHighlighter.h
|
||||||
|
syntax/MarkdownHighlighter.h
|
||||||
|
syntax/LispHighlighter.h
|
||||||
|
syntax/SqlHighlighter.h
|
||||||
|
syntax/ForthHighlighter.h
|
||||||
|
syntax/JsonHighlighter.h
|
||||||
|
syntax/TreeSitterHighlighter.h
|
||||||
|
syntax/NullHighlighter.h
|
||||||
|
syntax/CppHighlighter.h
|
||||||
|
syntax/ErlangHighlighter.h
|
||||||
|
syntax/LanguageHighlighter.h
|
||||||
|
syntax/RustHighlighter.h
|
||||||
|
syntax/PythonHighlighter.h
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
list(APPEND THEME_HEADERS
|
||||||
|
TreeSitterHighlighter.h)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(THEME_HEADERS
|
||||||
|
themes/ThemeHelpers.h
|
||||||
|
themes/EInk.h
|
||||||
|
themes/Gruvbox.h
|
||||||
|
themes/Solarized.h
|
||||||
|
themes/Plan9.h
|
||||||
|
themes/Nord.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(COMMON_HEADERS
|
set(COMMON_HEADERS
|
||||||
@@ -90,6 +152,10 @@ set(COMMON_HEADERS
|
|||||||
UndoNode.h
|
UndoNode.h
|
||||||
UndoTree.h
|
UndoTree.h
|
||||||
UndoSystem.h
|
UndoSystem.h
|
||||||
|
Highlight.h
|
||||||
|
|
||||||
|
${SYNTAX_HEADERS}
|
||||||
|
${THEME_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
# kte (terminal-first) executable
|
# kte (terminal-first) executable
|
||||||
@@ -108,6 +174,18 @@ endif ()
|
|||||||
|
|
||||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||||
|
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
# Users can provide their own tree-sitter include/lib via cache variables
|
||||||
|
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||||
|
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||||
|
if (TREESITTER_INCLUDE_DIR)
|
||||||
|
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||||
|
endif ()
|
||||||
|
if (TREESITTER_LIBRARY)
|
||||||
|
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
install(TARGETS kte
|
install(TARGETS kte
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
@@ -133,6 +211,14 @@ if (BUILD_TESTS)
|
|||||||
|
|
||||||
|
|
||||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
if (TREESITTER_INCLUDE_DIR)
|
||||||
|
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||||
|
endif ()
|
||||||
|
if (TREESITTER_LIBRARY)
|
||||||
|
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (${BUILD_GUI})
|
if (${BUILD_GUI})
|
||||||
|
|||||||
504
Command.cc
504
Command.cc
@@ -4,12 +4,21 @@
|
|||||||
#include <regex>
|
#include <regex>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include "HelpText.h"
|
#include "HelpText.h"
|
||||||
|
#include "syntax/LanguageHighlighter.h"
|
||||||
|
#include "syntax/HighlighterEngine.h"
|
||||||
|
#include "syntax/CppHighlighter.h"
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
#include "GUITheme.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Keep buffer viewport offsets so that the cursor stays within the visible
|
// Keep buffer viewport offsets so that the cursor stays within the visible
|
||||||
@@ -87,8 +96,10 @@ ensure_at_least_one_line(Buffer &buf)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Determine if a command mutates the buffer contents (text edits)
|
// Determine if a command mutates the buffer contents (text edits)
|
||||||
static bool is_mutating_command(CommandId id)
|
static bool
|
||||||
|
is_mutating_command(CommandId id)
|
||||||
{
|
{
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case CommandId::InsertText:
|
case CommandId::InsertText:
|
||||||
@@ -723,6 +734,21 @@ cmd_kprefix(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Start generic command prompt (": ")
|
||||||
|
static bool
|
||||||
|
cmd_command_prompt_start(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
// Close any pending edit batch before entering prompt
|
||||||
|
if (Buffer *b = ctx.editor.CurrentBuffer()) {
|
||||||
|
if (auto *u = b->Undo())
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
ctx.editor.StartPrompt(Editor::PromptKind::Command, "", "");
|
||||||
|
ctx.editor.SetStatus(": ");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_unknown_kcommand(CommandContext &ctx)
|
cmd_unknown_kcommand(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -737,8 +763,272 @@ cmd_unknown_kcommand(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- 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
|
static bool
|
||||||
cmd_find_start(CommandContext &ctx)
|
cmd_syntax(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
|
if (!b) {
|
||||||
|
ctx.editor.SetStatus("No buffer");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::string arg = ctx.arg;
|
||||||
|
// trim
|
||||||
|
auto trim = [](std::string &s) {
|
||||||
|
auto notsp = [](int ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
};
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||||
|
};
|
||||||
|
trim(arg);
|
||||||
|
if (arg == "on") {
|
||||||
|
b->SetSyntaxEnabled(true);
|
||||||
|
// If no highlighter but filetype is cpp by extension, set it
|
||||||
|
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||||
|
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus("syntax: on");
|
||||||
|
} else if (arg == "off") {
|
||||||
|
b->SetSyntaxEnabled(false);
|
||||||
|
ctx.editor.SetStatus("syntax: off");
|
||||||
|
} else if (arg == "reload") {
|
||||||
|
if (auto *eng = b->Highlighter())
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
ctx.editor.SetStatus("syntax: reloaded");
|
||||||
|
} else {
|
||||||
|
ctx.editor.SetStatus("usage: :syntax on|off|reload");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_set_option(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
|
if (!b) {
|
||||||
|
ctx.editor.SetStatus("No buffer");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Expect key=value
|
||||||
|
auto eq = ctx.arg.find('=');
|
||||||
|
if (eq == std::string::npos) {
|
||||||
|
ctx.editor.SetStatus("usage: :set key=value");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::string key = ctx.arg.substr(0, eq);
|
||||||
|
std::string val = ctx.arg.substr(eq + 1);
|
||||||
|
// trim
|
||||||
|
auto trim = [](std::string &s) {
|
||||||
|
auto notsp = [](int ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
};
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||||
|
};
|
||||||
|
trim(key);
|
||||||
|
trim(val);
|
||||||
|
// lower-case value for filetype
|
||||||
|
for (auto &ch: val)
|
||||||
|
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||||
|
if (key == "filetype") {
|
||||||
|
apply_filetype(*b, val);
|
||||||
|
if (b->SyntaxEnabled())
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype()));
|
||||||
|
else
|
||||||
|
ctx.editor.SetStatus("filetype: off");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus("unknown option: " + key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GUI theme cycling commands (available in GUI build; show message otherwise)
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
static bool
|
||||||
|
cmd_theme_next(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
auto id = kte::NextTheme();
|
||||||
|
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_theme_prev(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
auto id = kte::PrevTheme();
|
||||||
|
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool
|
||||||
|
cmd_theme_next(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
ctx.editor.SetStatus("Theme switching only available in GUI build");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_theme_prev(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
ctx.editor.SetStatus("Theme switching only available in GUI build");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Theme set by name command
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
static bool
|
||||||
|
cmd_theme_set_by_name(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
std::string name = ctx.arg;
|
||||||
|
// trim spaces
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(name);
|
||||||
|
rtrim(name);
|
||||||
|
if (name.empty()) {
|
||||||
|
ctx.editor.SetStatus("theme: missing name");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (kte::ApplyThemeByName(name)) {
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Theme: ") + name + std::string(" (bg: ") + kte::BackgroundModeName() + ")");
|
||||||
|
} else {
|
||||||
|
// Build list of available themes
|
||||||
|
const auto ® = kte::ThemeRegistry();
|
||||||
|
std::string avail;
|
||||||
|
for (size_t i = 0; i < reg.size(); ++i) {
|
||||||
|
if (i)
|
||||||
|
avail += ", ";
|
||||||
|
avail += reg[i]->Name();
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string("Unknown theme; available: ") + avail);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool
|
||||||
|
cmd_theme_set_by_name(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
(void) ctx;
|
||||||
|
// No-op in terminal build
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Background set command (GUI)
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
static bool
|
||||||
|
cmd_background_set(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
std::string mode = ctx.arg;
|
||||||
|
// trim
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(mode);
|
||||||
|
rtrim(mode);
|
||||||
|
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (mode != "light" && mode != "dark") {
|
||||||
|
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
kte::SetBackgroundMode(mode == "light" ? kte::BackgroundMode::Light : kte::BackgroundMode::Dark);
|
||||||
|
// Re-apply current theme to reflect background change
|
||||||
|
kte::ApplyThemeByName(kte::CurrentThemeName());
|
||||||
|
ctx.editor.SetStatus(std::string("Background: ") + mode + std::string("; Theme: ") + kte::CurrentThemeName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool
|
||||||
|
cmd_background_set(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
(void) ctx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_find_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@@ -761,7 +1051,7 @@ cmd_find_start(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_regex_find_start(CommandContext &ctx)
|
cmd_regex_find_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@@ -784,7 +1074,7 @@ cmd_regex_find_start(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_search_replace_start(CommandContext &ctx)
|
cmd_search_replace_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@@ -808,7 +1098,7 @@ cmd_search_replace_start(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_regex_replace_start(CommandContext &ctx)
|
cmd_regex_replace_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@@ -832,7 +1122,7 @@ cmd_regex_replace_start(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_open_file_start(CommandContext &ctx)
|
cmd_open_file_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
// Start a generic prompt to read a path
|
// Start a generic prompt to read a path
|
||||||
ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", "");
|
ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", "");
|
||||||
@@ -843,7 +1133,7 @@ cmd_open_file_start(CommandContext &ctx)
|
|||||||
|
|
||||||
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
|
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
|
||||||
static bool
|
static bool
|
||||||
cmd_visual_file_picker_toggle(CommandContext &ctx)
|
cmd_visual_file_picker_toggle(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
// Toggle visibility
|
// Toggle visibility
|
||||||
bool show = !ctx.editor.FilePickerVisible();
|
bool show = !ctx.editor.FilePickerVisible();
|
||||||
@@ -866,7 +1156,7 @@ cmd_visual_file_picker_toggle(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_jump_to_line_start(CommandContext &ctx)
|
cmd_jump_to_line_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
// Start a prompt to read a 1-based line number and jump there (clamped)
|
// Start a prompt to read a 1-based line number and jump there (clamped)
|
||||||
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
|
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
|
||||||
@@ -877,7 +1167,7 @@ cmd_jump_to_line_start(CommandContext &ctx)
|
|||||||
|
|
||||||
// --- Buffers: switch/next/prev/close ---
|
// --- Buffers: switch/next/prev/close ---
|
||||||
static bool
|
static bool
|
||||||
cmd_buffer_switch_start(CommandContext &ctx)
|
cmd_buffer_switch_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
// If only one (or zero) buffer is open, do nothing per spec
|
// If only one (or zero) buffer is open, do nothing per spec
|
||||||
if (ctx.editor.BufferCount() <= 1) {
|
if (ctx.editor.BufferCount() <= 1) {
|
||||||
@@ -895,7 +1185,7 @@ buffer_display_name(const Buffer &b)
|
|||||||
{
|
{
|
||||||
if (!b.Filename().empty())
|
if (!b.Filename().empty())
|
||||||
return b.Filename();
|
return b.Filename();
|
||||||
return std::string("<untitled>");
|
return {"<untitled>"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -904,7 +1194,7 @@ buffer_basename(const Buffer &b)
|
|||||||
{
|
{
|
||||||
const std::string &p = b.Filename();
|
const std::string &p = b.Filename();
|
||||||
if (p.empty())
|
if (p.empty())
|
||||||
return std::string("<untitled>");
|
return {"<untitled>"};
|
||||||
auto pos = p.find_last_of("/\\");
|
auto pos = p.find_last_of("/\\");
|
||||||
if (pos == std::string::npos)
|
if (pos == std::string::npos)
|
||||||
return p;
|
return p;
|
||||||
@@ -913,7 +1203,7 @@ buffer_basename(const Buffer &b)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_buffer_next(CommandContext &ctx)
|
cmd_buffer_next(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
const auto cnt = ctx.editor.BufferCount();
|
const auto cnt = ctx.editor.BufferCount();
|
||||||
if (cnt <= 1) {
|
if (cnt <= 1) {
|
||||||
@@ -930,7 +1220,7 @@ cmd_buffer_next(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_buffer_prev(CommandContext &ctx)
|
cmd_buffer_prev(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
const auto cnt = ctx.editor.BufferCount();
|
const auto cnt = ctx.editor.BufferCount();
|
||||||
if (cnt <= 1) {
|
if (cnt <= 1) {
|
||||||
@@ -947,7 +1237,7 @@ cmd_buffer_prev(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_buffer_close(CommandContext &ctx)
|
cmd_buffer_close(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
if (ctx.editor.BufferCount() == 0)
|
if (ctx.editor.BufferCount() == 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -1116,6 +1406,85 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
|
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic command prompt completion
|
||||||
|
if (kind == Editor::PromptKind::Command) {
|
||||||
|
std::string text = ctx.editor.PromptText();
|
||||||
|
// Split into command and arg prefix
|
||||||
|
auto sp = text.find(' ');
|
||||||
|
if (sp == std::string::npos) {
|
||||||
|
// complete command name from public commands
|
||||||
|
std::string prefix = text;
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const auto &c: CommandRegistry::All()) {
|
||||||
|
if (c.isPublic) {
|
||||||
|
if (prefix.empty() || c.name.rfind(prefix, 0) == 0)
|
||||||
|
names.push_back(c.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (names.empty()) {
|
||||||
|
// no change
|
||||||
|
} else if (names.size() == 1) {
|
||||||
|
ctx.editor.SetPromptText(names[0]);
|
||||||
|
} else {
|
||||||
|
// compute LCP
|
||||||
|
std::string lcp = names[0];
|
||||||
|
for (size_t i = 1; i < names.size(); ++i) {
|
||||||
|
const std::string &s = names[i];
|
||||||
|
size_t j = 0;
|
||||||
|
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
|
||||||
|
++j;
|
||||||
|
lcp.resize(j);
|
||||||
|
if (lcp.empty())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!lcp.empty() && lcp != text)
|
||||||
|
ctx.editor.SetPromptText(lcp);
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
std::string cmd = text.substr(0, sp);
|
||||||
|
std::string argprefix = text.substr(sp + 1);
|
||||||
|
// Only special-case argument completion for certain commands
|
||||||
|
if (cmd == "theme") {
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
std::vector<std::string> cands;
|
||||||
|
const auto ® = kte::ThemeRegistry();
|
||||||
|
for (const auto &t: reg) {
|
||||||
|
std::string n = t->Name();
|
||||||
|
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
||||||
|
cands.push_back(n);
|
||||||
|
}
|
||||||
|
if (cands.empty()) {
|
||||||
|
// no change
|
||||||
|
} else if (cands.size() == 1) {
|
||||||
|
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
|
||||||
|
} else {
|
||||||
|
std::string lcp = cands[0];
|
||||||
|
for (size_t i = 1; i < cands.size(); ++i) {
|
||||||
|
const std::string &s = cands[i];
|
||||||
|
size_t j = 0;
|
||||||
|
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
|
||||||
|
++j;
|
||||||
|
lcp.resize(j);
|
||||||
|
if (lcp.empty())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!lcp.empty() && lcp != argprefix)
|
||||||
|
ctx.editor.SetPromptText(cmd + std::string(" ") + lcp);
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void) argprefix; // no completion in non-GUI build
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
// default: no special arg completion
|
||||||
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.editor.AppendPromptText(ctx.arg);
|
ctx.editor.AppendPromptText(ctx.arg);
|
||||||
@@ -1223,6 +1592,7 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Toggle read-only state of the current buffer
|
// Toggle read-only state of the current buffer
|
||||||
static bool
|
static bool
|
||||||
cmd_toggle_read_only(CommandContext &ctx)
|
cmd_toggle_read_only(CommandContext &ctx)
|
||||||
@@ -1258,8 +1628,10 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
std::string line;
|
std::string line;
|
||||||
auto unquote = [](std::string s) {
|
auto unquote = [](std::string s) {
|
||||||
if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin());
|
if (!s.empty() && (s.front() == '"' || s.front() == '\''))
|
||||||
if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back();
|
s.erase(s.begin());
|
||||||
|
if (!s.empty() && (s.back() == '"' || s.back() == '\''))
|
||||||
|
s.pop_back();
|
||||||
return s;
|
return s;
|
||||||
};
|
};
|
||||||
while (std::getline(iss, line)) {
|
while (std::getline(iss, line)) {
|
||||||
@@ -1275,17 +1647,20 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
std::string title;
|
std::string title;
|
||||||
std::getline(ls, title);
|
std::getline(ls, title);
|
||||||
// trim leading spaces
|
// trim leading spaces
|
||||||
while (!title.empty() && (title.front() == ' ' || title.front() == '\t')) title.erase(title.begin());
|
while (!title.empty() && (title.front() == ' ' || title.front() == '\t'))
|
||||||
|
title.erase(title.begin());
|
||||||
title = unquote(title);
|
title = unquote(title);
|
||||||
out << "\n\n";
|
out << "\n\n";
|
||||||
for (auto &c : title) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
for (auto &c: title)
|
||||||
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
out << title << "\n";
|
out << title << "\n";
|
||||||
} else if (macro == "PP" || macro == "P" || macro == "TP") {
|
} else if (macro == "PP" || macro == "P" || macro == "TP") {
|
||||||
out << "\n";
|
out << "\n";
|
||||||
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
|
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
|
||||||
std::string rest;
|
std::string rest;
|
||||||
std::getline(ls, rest);
|
std::getline(ls, rest);
|
||||||
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) rest.erase(rest.begin());
|
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
|
||||||
|
rest.erase(rest.begin());
|
||||||
out << unquote(rest) << "\n";
|
out << unquote(rest) << "\n";
|
||||||
} else if (macro == "nf" || macro == "fi") {
|
} else if (macro == "nf" || macro == "fi") {
|
||||||
// ignore fill mode toggles for now
|
// ignore fill mode toggles for now
|
||||||
@@ -1297,11 +1672,23 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
// Regular text; apply minimal escape replacements
|
// Regular text; apply minimal escape replacements
|
||||||
for (std::size_t i = 0; i < line.size(); ++i) {
|
for (std::size_t i = 0; i < line.size(); ++i) {
|
||||||
if (line[i] == '\\') {
|
if (line[i] == '\\') {
|
||||||
if (i + 1 < line.size() && line[i + 1] == '-') { out << '-'; ++i; continue; }
|
if (i + 1 < line.size() && line[i + 1] == '-') {
|
||||||
|
out << '-';
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (i + 3 < line.size() && line[i + 1] == '(') {
|
if (i + 3 < line.size() && line[i + 1] == '(') {
|
||||||
std::string esc = line.substr(i + 2, 2);
|
std::string esc = line.substr(i + 2, 2);
|
||||||
if (esc == "em") { out << "—"; i += 3; continue; }
|
if (esc == "em") {
|
||||||
if (esc == "en") { out << "-"; i += 3; continue; }
|
out << "—";
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (esc == "en") {
|
||||||
|
out << "-";
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out << line[i];
|
out << line[i];
|
||||||
@@ -1315,7 +1702,10 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
// 1) Prefer embedded/customizable help content
|
// 1) Prefer embedded/customizable help content
|
||||||
{
|
{
|
||||||
std::string embedded = HelpText::Text();
|
std::string embedded = HelpText::Text();
|
||||||
if (!embedded.empty()) { used_man = false; return embedded; }
|
if (!embedded.empty()) {
|
||||||
|
used_man = false;
|
||||||
|
return embedded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Fall back to the manpage and convert roff to plain text
|
// 2) Fall back to the manpage and convert roff to plain text
|
||||||
@@ -1329,7 +1719,10 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
std::ifstream in(p);
|
std::ifstream in(p);
|
||||||
if (in.good()) {
|
if (in.good()) {
|
||||||
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||||
if (!s.empty()) { used_man = true; return roff_to_text(s); }
|
if (!s.empty()) {
|
||||||
|
used_man = true;
|
||||||
|
return roff_to_text(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback minimal help text
|
// Fallback minimal help text
|
||||||
@@ -1430,6 +1823,46 @@ cmd_newline(CommandContext &ctx)
|
|||||||
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
|
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
|
||||||
std::string value = ctx.editor.PromptText();
|
std::string value = ctx.editor.PromptText();
|
||||||
ctx.editor.AcceptPrompt();
|
ctx.editor.AcceptPrompt();
|
||||||
|
if (kind == Editor::PromptKind::Command) {
|
||||||
|
// Parse COMMAND ARG and dispatch only public commands
|
||||||
|
// Trim leading/trailing spaces
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(value);
|
||||||
|
rtrim(value);
|
||||||
|
if (value.empty()) {
|
||||||
|
ctx.editor.SetStatus("Canceled");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Split first token
|
||||||
|
std::string cmdname;
|
||||||
|
std::string arg;
|
||||||
|
auto sp = value.find(' ');
|
||||||
|
if (sp == std::string::npos) {
|
||||||
|
cmdname = value;
|
||||||
|
} else {
|
||||||
|
cmdname = value.substr(0, sp);
|
||||||
|
arg = value.substr(sp + 1);
|
||||||
|
}
|
||||||
|
const Command *cmd = CommandRegistry::FindByName(cmdname);
|
||||||
|
if (!cmd || !cmd->isPublic) {
|
||||||
|
ctx.editor.SetStatus(std::string("Unknown command: ") + cmdname);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ok = Execute(ctx.editor, cmdname, arg);
|
||||||
|
if (!ok) {
|
||||||
|
ctx.editor.SetStatus(std::string("Command failed: ") + cmdname);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
|
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
|
||||||
// Finish search: keep cursor where it is, clear search UI prompt
|
// Finish search: keep cursor where it is, clear search UI prompt
|
||||||
ctx.editor.SetSearchActive(false);
|
ctx.editor.SetSearchActive(false);
|
||||||
@@ -3275,7 +3708,25 @@ InstallDefaultCommands()
|
|||||||
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
|
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
|
||||||
});
|
});
|
||||||
// Read-only
|
// Read-only
|
||||||
CommandRegistry::Register({CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only});
|
CommandRegistry::Register({
|
||||||
|
CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only
|
||||||
|
});
|
||||||
|
// GUI Themes
|
||||||
|
CommandRegistry::Register({CommandId::ThemeNext, "theme-next", "Cycle to next GUI theme", cmd_theme_next});
|
||||||
|
CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev});
|
||||||
|
// Theme by name (public in command prompt)
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true
|
||||||
|
});
|
||||||
|
// Background light/dark (public)
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true
|
||||||
|
});
|
||||||
|
// Generic command prompt (C-k ;)
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt",
|
||||||
|
cmd_command_prompt_start
|
||||||
|
});
|
||||||
// Buffer operations
|
// Buffer operations
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
||||||
@@ -3305,6 +3756,9 @@ InstallDefaultCommands()
|
|||||||
// UI helpers
|
// UI helpers
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||||
|
// Syntax highlighting (public commands)
|
||||||
|
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||||
|
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
Command.h
14
Command.h
@@ -69,6 +69,9 @@ enum class CommandId {
|
|||||||
Redo,
|
Redo,
|
||||||
// UI/status helpers
|
// UI/status helpers
|
||||||
UArgStatus, // update status line during universal-argument collection
|
UArgStatus, // update status line during universal-argument collection
|
||||||
|
// Themes (GUI)
|
||||||
|
ThemeNext,
|
||||||
|
ThemePrev,
|
||||||
// Region formatting
|
// Region formatting
|
||||||
IndentRegion, // indent region (C-k =)
|
IndentRegion, // indent region (C-k =)
|
||||||
UnindentRegion, // unindent region (C-k -)
|
UnindentRegion, // unindent region (C-k -)
|
||||||
@@ -86,6 +89,15 @@ enum class CommandId {
|
|||||||
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||||
// Meta
|
// Meta
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
|
// Generic command prompt
|
||||||
|
CommandPromptStart, // begin generic command prompt (C-k ;)
|
||||||
|
// Theme by name
|
||||||
|
ThemeSetByName,
|
||||||
|
// Background mode (GUI)
|
||||||
|
BackgroundSet,
|
||||||
|
// Syntax highlighting
|
||||||
|
Syntax, // ":syntax on|off|reload"
|
||||||
|
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +121,8 @@ struct Command {
|
|||||||
std::string name; // stable, unique name (e.g., "save", "save-as")
|
std::string name; // stable, unique name (e.g., "save", "save-as")
|
||||||
std::string help; // short help/description
|
std::string help; // short help/description
|
||||||
CommandHandler handler;
|
CommandHandler handler;
|
||||||
|
// Public commands are exposed in the ": " prompt (C-k ;)
|
||||||
|
bool isPublic = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
75
Editor.cc
75
Editor.cc
@@ -3,6 +3,9 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/CppHighlighter.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
Editor::Editor() = default;
|
Editor::Editor() = default;
|
||||||
@@ -151,7 +154,32 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
const bool rows_empty = rows.empty();
|
const bool rows_empty = rows.empty();
|
||||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||||
return cur.OpenFromFile(path, err);
|
bool ok = cur.OpenFromFile(path, err);
|
||||||
|
if (!ok)
|
||||||
|
return false;
|
||||||
|
// Setup highlighting using registry (extension + shebang)
|
||||||
|
cur.EnsureHighlighter();
|
||||||
|
std::string first = "";
|
||||||
|
const auto &rows = cur.Rows();
|
||||||
|
if (!rows.empty())
|
||||||
|
first = static_cast<std::string>(rows[0]);
|
||||||
|
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||||
|
if (!ft.empty()) {
|
||||||
|
cur.SetFiletype(ft);
|
||||||
|
cur.SetSyntaxEnabled(true);
|
||||||
|
if (auto *eng = cur.Highlighter()) {
|
||||||
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cur.SetFiletype("");
|
||||||
|
cur.SetSyntaxEnabled(true);
|
||||||
|
if (auto *eng = cur.Highlighter()) {
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +187,30 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
if (!b.OpenFromFile(path, err)) {
|
if (!b.OpenFromFile(path, err)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||||
|
b.EnsureHighlighter();
|
||||||
|
std::string first = "";
|
||||||
|
{
|
||||||
|
const auto &rows = b.Rows();
|
||||||
|
if (!rows.empty())
|
||||||
|
first = static_cast<std::string>(rows[0]);
|
||||||
|
}
|
||||||
|
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||||
|
if (!ft.empty()) {
|
||||||
|
b.SetFiletype(ft);
|
||||||
|
b.SetSyntaxEnabled(true);
|
||||||
|
if (auto *eng = b.Highlighter()) {
|
||||||
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.SetFiletype("");
|
||||||
|
b.SetSyntaxEnabled(true);
|
||||||
|
if (auto *eng = b.Highlighter()) {
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Add as a new buffer and switch to it
|
// Add as a new buffer and switch to it
|
||||||
std::size_t idx = AddBuffer(std::move(b));
|
std::size_t idx = AddBuffer(std::move(b));
|
||||||
SwitchTo(idx);
|
SwitchTo(idx);
|
||||||
@@ -173,6 +225,27 @@ Editor::SwitchTo(std::size_t index)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
curbuf_ = index;
|
curbuf_ = index;
|
||||||
|
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||||
|
Buffer &b = buffers_[curbuf_];
|
||||||
|
if (b.SyntaxEnabled()) {
|
||||||
|
b.EnsureHighlighter();
|
||||||
|
if (auto *eng = b.Highlighter()) {
|
||||||
|
if (!eng->HasHighlighter()) {
|
||||||
|
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||||
|
if (!b.Filetype().empty()) {
|
||||||
|
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||||
|
if (hl) {
|
||||||
|
eng->SetHighlighter(std::move(hl));
|
||||||
|
} else {
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
}
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
Editor.h
29
Editor.h
@@ -315,7 +315,8 @@ public:
|
|||||||
GotoLine,
|
GotoLine,
|
||||||
Chdir,
|
Chdir,
|
||||||
ReplaceFind, // step 1 of Search & Replace: find what
|
ReplaceFind, // step 1 of Search & Replace: find what
|
||||||
ReplaceWith // step 2 of Search & Replace: replace with
|
ReplaceWith, // step 2 of Search & Replace: replace with
|
||||||
|
Command // generic command prompt (": ")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -524,10 +525,28 @@ private:
|
|||||||
|
|
||||||
// Temporary state for Search & Replace flow
|
// Temporary state for Search & Replace flow
|
||||||
public:
|
public:
|
||||||
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
|
void SetReplaceFindTmp(const std::string &s)
|
||||||
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
|
{
|
||||||
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
|
replace_find_tmp_ = s;
|
||||||
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetReplaceWithTmp(const std::string &s)
|
||||||
|
{
|
||||||
|
replace_with_tmp_ = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceFindTmp() const
|
||||||
|
{
|
||||||
|
return replace_find_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceWithTmp() const
|
||||||
|
{
|
||||||
|
return replace_with_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string replace_find_tmp_;
|
std::string replace_find_tmp_;
|
||||||
|
|||||||
19
GUIConfig.cc
19
GUIConfig.cc
@@ -102,6 +102,25 @@ GUIConfig::LoadFromFile(const std::string &path)
|
|||||||
if (v > 0.0f) {
|
if (v > 0.0f) {
|
||||||
font_size = v;
|
font_size = v;
|
||||||
}
|
}
|
||||||
|
} else if (key == "theme") {
|
||||||
|
theme = val;
|
||||||
|
} else if (key == "background" || key == "bg") {
|
||||||
|
std::string v = val;
|
||||||
|
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (v == "light" || v == "dark")
|
||||||
|
background = v;
|
||||||
|
} else if (key == "syntax") {
|
||||||
|
std::string v = val;
|
||||||
|
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (v == "1" || v == "on" || v == "true" || v == "yes") {
|
||||||
|
syntax = true;
|
||||||
|
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
|
||||||
|
syntax = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public:
|
|||||||
int columns = 80;
|
int columns = 80;
|
||||||
int rows = 42;
|
int rows = 42;
|
||||||
float font_size = (float) KTE_FONT_SIZE;
|
float font_size = (float) KTE_FONT_SIZE;
|
||||||
|
std::string theme = "nord";
|
||||||
|
// Background mode for themes that support light/dark variants
|
||||||
|
// Values: "dark" (default), "light"
|
||||||
|
std::string background = "dark";
|
||||||
|
|
||||||
|
// Default syntax highlighting state for GUI (kge): on/off
|
||||||
|
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||||
|
bool syntax = true; // default: enabled
|
||||||
|
|
||||||
// Load from default path: $HOME/.config/kte/kge.ini
|
// Load from default path: $HOME/.config/kte/kge.ini
|
||||||
static GUIConfig Load();
|
static GUIConfig Load();
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
#include "Font.h" // embedded default font (DefaultFontRegular)
|
#include "Font.h" // embedded default font (DefaultFontRegular)
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
#include "GUITheme.h"
|
#include "GUITheme.h"
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
#ifndef KTE_FONT_SIZE
|
#ifndef KTE_FONT_SIZE
|
||||||
@@ -32,8 +34,8 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load GUI configuration (fullscreen, columns/rows, font size)
|
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||||
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load();
|
GUIConfig cfg = GUIConfig::Load();
|
||||||
|
|
||||||
// GL attributes for core profile
|
// GL attributes for core profile
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||||
@@ -47,7 +49,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
// Compute desired window size from config
|
// Compute desired window size from config
|
||||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
|
|
||||||
if (fullscreen) {
|
if (cfg.fullscreen) {
|
||||||
// "Fullscreen": fill the usable bounds of the primary display.
|
// "Fullscreen": fill the usable bounds of the primary display.
|
||||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
@@ -61,8 +63,8 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
||||||
int w = static_cast<int>(columns * font_size);
|
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
||||||
int h = static_cast<int>((rows * 2) * font_size);
|
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
||||||
|
|
||||||
// As a safety, clamp to display usable bounds if retrievable
|
// As a safety, clamp to display usable bounds if retrievable
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
@@ -86,7 +88,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
// macOS: when "fullscreen" is requested, position the window at the
|
// macOS: when "fullscreen" is requested, position the window at the
|
||||||
// top-left of the usable display area to mimic fullscreen while keeping
|
// top-left of the usable display area to mimic fullscreen while keeping
|
||||||
// the system menu bar visible.
|
// the system menu bar visible.
|
||||||
if (fullscreen) {
|
if (cfg.fullscreen) {
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
||||||
@@ -105,8 +107,45 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
(void) io;
|
(void) io;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
// Apply a Nord-inspired theme
|
|
||||||
kte::ApplyNordImGuiTheme();
|
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||||
|
if (cfg.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
kte::ApplyThemeByName(cfg.theme);
|
||||||
|
|
||||||
|
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||||
|
if (Buffer *b = ed.CurrentBuffer()) {
|
||||||
|
if (cfg.syntax) {
|
||||||
|
b->SetSyntaxEnabled(true);
|
||||||
|
// Ensure a highlighter is available if possible
|
||||||
|
b->EnsureHighlighter();
|
||||||
|
if (auto *eng = b->Highlighter()) {
|
||||||
|
if (!eng->HasHighlighter()) {
|
||||||
|
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||||
|
std::string first_line;
|
||||||
|
const auto &rows = b->Rows();
|
||||||
|
if (!rows.empty())
|
||||||
|
first_line = static_cast<std::string>(rows[0]);
|
||||||
|
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||||
|
b->Filename(), first_line);
|
||||||
|
if (!ft.empty()) {
|
||||||
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||||
|
b->SetFiletype(ft);
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
} else {
|
||||||
|
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
b->SetFiletype("");
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b->SetSyntaxEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||||
return false;
|
return false;
|
||||||
@@ -135,7 +174,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Initialize GUI font from embedded default (use configured size or compiled default)
|
// Initialize GUI font from embedded default (use configured size or compiled default)
|
||||||
LoadGuiFont_(nullptr, (float) font_size);
|
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -214,7 +253,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||||
|
|
||||||
// Visible content rows inside the scroll child
|
// Visible content rows inside the scroll child
|
||||||
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
auto content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||||
// Editor::Rows includes the status line; add 1 back for it.
|
// Editor::Rows includes the status line; add 1 back for it.
|
||||||
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
||||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||||
@@ -264,11 +303,11 @@ GUIFrontend::Shutdown()
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||||
{
|
{
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
const ImGuiIO &io = ImGui::GetIO();
|
||||||
io.Fonts->Clear();
|
io.Fonts->Clear();
|
||||||
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||||
(void *) DefaultFontBoldCompressedData,
|
DefaultFontBoldCompressedData,
|
||||||
(int) DefaultFontBoldCompressedSize,
|
DefaultFontBoldCompressedSize,
|
||||||
size_px);
|
size_px);
|
||||||
if (!font) {
|
if (!font) {
|
||||||
font = io.Fonts->AddFontDefault();
|
font = io.Fonts->AddFontDefault();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIInputHandler input_{};
|
GUIInputHandler input_{};
|
||||||
GUIRenderer renderer_{};
|
GUIRenderer renderer_{};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
#include "GUIInputHandler.h"
|
#include "GUIInputHandler.h"
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
@@ -92,10 +93,14 @@ map_key(const SDL_Keycode key,
|
|||||||
out = {true, CommandId::Backspace, "", 0};
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_TAB:
|
case SDLK_TAB:
|
||||||
// Do not insert text on KEYDOWN; allow SDL_TEXTINPUT to deliver '\t'
|
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||||
// as printable input so that all printable characters flow via TEXTINPUT.
|
// If k-prefix is active, let the k-prefix handler below consume the key
|
||||||
out.hasCommand = false;
|
// (so Tab doesn't leave k-prefix stuck).
|
||||||
|
if (!k_prefix) {
|
||||||
|
out = {true, CommandId::InsertText, std::string("\t"), 0};
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
break; // fall through so k-prefix handler can process
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
out = {true, CommandId::Newline, "", 0};
|
out = {true, CommandId::Newline, "", 0};
|
||||||
@@ -280,6 +285,14 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
bool produced = false;
|
bool produced = false;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_MOUSEWHEEL: {
|
case SDL_MOUSEWHEEL: {
|
||||||
|
// If ImGui wants to capture the mouse (e.g., hovering the File Picker list),
|
||||||
|
// don't translate wheel events into editor scrolling.
|
||||||
|
// This prevents background buffer scroll while using GUI widgets.
|
||||||
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
if (io.WantCaptureMouse) {
|
||||||
|
return true; // consumed by GUI
|
||||||
|
}
|
||||||
|
|
||||||
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
|
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
|
||||||
int dy = e.wheel.y;
|
int dy = e.wheel.y;
|
||||||
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
||||||
@@ -347,6 +360,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
uarg_text_,
|
uarg_text_,
|
||||||
mi);
|
mi);
|
||||||
|
|
||||||
|
// If we inserted a TAB on KEYDOWN, suppress any subsequent SDL_TEXTINPUT
|
||||||
|
// for this keystroke to avoid double insertion on platforms that emit it.
|
||||||
|
if (produced && mi.hasCommand && mi.id == CommandId::InsertText && mi.arg == "\t") {
|
||||||
|
suppress_text_input_once_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
||||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include "GUIRenderer.h"
|
#include "GUIRenderer.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
#include "GUITheme.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
@@ -44,6 +46,8 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGui::SetNextWindowSize(main_sz);
|
ImGui::SetNextWindowSize(main_sz);
|
||||||
|
|
||||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
||||||
|
| ImGuiWindowFlags_NoScrollbar
|
||||||
|
| ImGuiWindowFlags_NoScrollWithMouse
|
||||||
| ImGuiWindowFlags_NoResize
|
| ImGuiWindowFlags_NoResize
|
||||||
| ImGuiWindowFlags_NoMove
|
| ImGuiWindowFlags_NoMove
|
||||||
| ImGuiWindowFlags_NoCollapse
|
| ImGuiWindowFlags_NoCollapse
|
||||||
@@ -151,6 +155,12 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
last_row = first_row + vis_rows - 1;
|
last_row = first_row + vis_rows - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||||
|
int fr = static_cast<int>(std::max(0L, first_row));
|
||||||
|
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||||
|
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
@@ -257,7 +267,9 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||||
if (search_mode) {
|
if (search_mode) {
|
||||||
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
||||||
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
if (ed.PromptActive() && (
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
try {
|
try {
|
||||||
std::regex rx(ed.SearchQuery());
|
std::regex rx(ed.SearchQuery());
|
||||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||||
@@ -302,23 +314,26 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
std::size_t rx_start = src_to_rx(sx);
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
std::size_t rx_end = src_to_rx(ex);
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
// Apply horizontal scroll offset
|
// Apply horizontal scroll offset
|
||||||
if (rx_end <= coloffs_now) continue; // fully left of view
|
if (rx_end <= coloffs_now)
|
||||||
|
continue; // fully left of view
|
||||||
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
||||||
std::size_t vx1 = rx_end - coloffs_now;
|
std::size_t vx1 = rx_end - coloffs_now;
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
|
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||||
|
line_pos.y + line_h);
|
||||||
// Choose color: current match stronger
|
// Choose color: current match stronger
|
||||||
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||||
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
|
ImU32 col = is_current
|
||||||
|
? IM_COL32(255, 220, 120, 140)
|
||||||
|
: IM_COL32(200, 200, 0, 90);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Emit entire line (ImGui child scrolling will handle clipping)
|
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||||
char c = line[src];
|
char c = line[src];
|
||||||
if (c == '\t') {
|
if (c == '\t') {
|
||||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||||
// Emit spaces for the tab
|
|
||||||
expanded.append(adv, ' ');
|
expanded.append(adv, ' ');
|
||||||
rx_abs_draw += adv;
|
rx_abs_draw += adv;
|
||||||
} else {
|
} else {
|
||||||
@@ -327,7 +342,43 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw syntax-colored runs (text above background highlights)
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||||
|
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
||||||
|
*buf, static_cast<int>(i), buf->Version());
|
||||||
|
// Helper to convert a src column to expanded rx position
|
||||||
|
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||||
|
std::size_t rx = 0;
|
||||||
|
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||||
|
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||||
|
}
|
||||||
|
return rx;
|
||||||
|
};
|
||||||
|
for (const auto &sp: lh.spans) {
|
||||||
|
std::size_t rx_s = src_to_rx_full(
|
||||||
|
static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||||
|
std::size_t rx_e = src_to_rx_full(
|
||||||
|
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||||
|
if (rx_e <= coloffs_now)
|
||||||
|
continue;
|
||||||
|
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||||
|
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||||
|
if (vx0 >= expanded.size())
|
||||||
|
continue;
|
||||||
|
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||||
|
if (vx1 <= vx0)
|
||||||
|
continue;
|
||||||
|
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||||
|
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||||
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
|
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||||
|
}
|
||||||
|
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
|
||||||
|
} else {
|
||||||
|
// No syntax: draw as one run
|
||||||
ImGui::TextUnformatted(expanded.c_str());
|
ImGui::TextUnformatted(expanded.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Draw a visible cursor indicator on the current line
|
// Draw a visible cursor indicator on the current line
|
||||||
if (i == cy) {
|
if (i == cy) {
|
||||||
@@ -390,13 +441,18 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
float max_px = std::max(0.0f, right_x - left_x);
|
float max_px = std::max(0.0f, right_x - left_x);
|
||||||
|
|
||||||
std::string prefix;
|
std::string prefix;
|
||||||
if (!label.empty()) prefix = label + ": ";
|
if (kind == Editor::PromptKind::Command) {
|
||||||
|
prefix = ": ";
|
||||||
|
} else if (!label.empty()) {
|
||||||
|
prefix = label + ": ";
|
||||||
|
}
|
||||||
|
|
||||||
// Compose showing right-end of filename portion when too long for space
|
// Compose showing right-end of filename portion when too long for space
|
||||||
std::string final_msg;
|
std::string final_msg;
|
||||||
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
||||||
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
||||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
|
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
||||||
// Trim from left until it fits by pixel width
|
// Trim from left until it fits by pixel width
|
||||||
std::string tail = ptext;
|
std::string tail = ptext;
|
||||||
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
||||||
@@ -408,7 +464,11 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
while (start < tail.size()) {
|
while (start < tail.size()) {
|
||||||
// Estimate how many chars to skip based on ratio
|
// Estimate how many chars to skip based on ratio
|
||||||
float ratio = tail_sz.x / avail_px;
|
float ratio = tail_sz.x / avail_px;
|
||||||
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
|
size_t skip = ratio > 1.5f
|
||||||
|
? std::min(tail.size() - start,
|
||||||
|
(size_t) std::max<size_t>(
|
||||||
|
1, (size_t) (tail.size() / 4)))
|
||||||
|
: 1;
|
||||||
start += skip;
|
start += skip;
|
||||||
std::string candidate = tail.substr(start);
|
std::string candidate = tail.substr(start);
|
||||||
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
||||||
@@ -425,7 +485,10 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
while (lo < hi) {
|
while (lo < hi) {
|
||||||
size_t mid = (lo + hi) / 2;
|
size_t mid = (lo + hi) / 2;
|
||||||
std::string cand = tail.substr(mid);
|
std::string cand = tail.substr(mid);
|
||||||
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
|
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
|
||||||
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
}
|
}
|
||||||
tail = tail.substr(lo);
|
tail = tail.substr(lo);
|
||||||
}
|
}
|
||||||
@@ -529,7 +592,8 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw right
|
// Draw right
|
||||||
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
|
||||||
|
p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(right.c_str());
|
ImGui::TextUnformatted(right.c_str());
|
||||||
|
|
||||||
// Draw middle message centered in remaining space
|
// Draw middle message centered in remaining space
|
||||||
|
|||||||
484
GUITheme.h
484
GUITheme.h
@@ -1,127 +1,415 @@
|
|||||||
// GUITheme.h - ImGui theme configuration for kte GUI
|
// GUITheme.h — ImGui theming helpers and background mode
|
||||||
// Provides a Nord-inspired color palette and style settings.
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include "themes/ThemeHelpers.h"
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
// Convert RGB hex (0xRRGGBB) to ImVec4 with optional alpha
|
// Background mode selection for light/dark palettes
|
||||||
static inline ImVec4
|
enum class BackgroundMode { Light, Dark };
|
||||||
RGBA(unsigned int rgb, float a = 1.0f)
|
|
||||||
|
// Global background mode; default to Dark to match prior defaults
|
||||||
|
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||||
|
|
||||||
|
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||||
|
enum class ThemeId {
|
||||||
|
EInk = 0,
|
||||||
|
GruvboxDarkMedium = 1,
|
||||||
|
GruvboxLightMedium = 1, // alias to unified gruvbox index
|
||||||
|
Nord = 2,
|
||||||
|
Plan9 = 3,
|
||||||
|
Solarized = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current theme tracking
|
||||||
|
static inline auto gCurrentTheme = ThemeId::Nord;
|
||||||
|
static inline std::size_t gCurrentThemeIndex = 0;
|
||||||
|
|
||||||
|
// Forward declarations for helpers used below
|
||||||
|
static size_t ThemeIndexFromId(ThemeId id);
|
||||||
|
|
||||||
|
static ThemeId ThemeIdFromIndex(size_t idx);
|
||||||
|
|
||||||
|
// Helpers to set/query background mode
|
||||||
|
static void
|
||||||
|
SetBackgroundMode(const BackgroundMode m)
|
||||||
{
|
{
|
||||||
float r = ((rgb >> 16) & 0xFF) / 255.0f;
|
gBackgroundMode = m;
|
||||||
float g = ((rgb >> 8) & 0xFF) / 255.0f;
|
|
||||||
float b = ((rgb) & 0xFF) / 255.0f;
|
|
||||||
return ImVec4(r, g, b, a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Apply a Nord-inspired theme to the current ImGui style.
|
static BackgroundMode
|
||||||
// Safe to call after ImGui::CreateContext().
|
GetBackgroundMode()
|
||||||
static inline void
|
|
||||||
ApplyNordImGuiTheme()
|
|
||||||
{
|
{
|
||||||
// Nord palette
|
return gBackgroundMode;
|
||||||
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
|
}
|
||||||
const ImVec4 nord1 = RGBA(0x3B4252);
|
|
||||||
const ImVec4 nord2 = RGBA(0x434C5E);
|
|
||||||
const ImVec4 nord3 = RGBA(0x4C566A);
|
|
||||||
const ImVec4 nord4 = RGBA(0xD8DEE9);
|
|
||||||
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
|
|
||||||
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
|
|
||||||
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
|
|
||||||
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
|
|
||||||
const ImVec4 nord12 = RGBA(0xD08770); // orange
|
|
||||||
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
|
|
||||||
|
|
||||||
ImGuiStyle &style = ImGui::GetStyle();
|
|
||||||
|
|
||||||
// Base style tweaks to suit Nord aesthetics
|
static inline const char *
|
||||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
BackgroundModeName()
|
||||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
{
|
||||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
return gBackgroundMode == BackgroundMode::Light ? "light" : "dark";
|
||||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
}
|
||||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
|
||||||
style.ScrollbarSize = 14.0f;
|
|
||||||
style.GrabMinSize = 10.0f;
|
|
||||||
style.WindowRounding = 4.0f;
|
|
||||||
style.FrameRounding = 3.0f;
|
|
||||||
style.PopupRounding = 4.0f;
|
|
||||||
style.GrabRounding = 3.0f;
|
|
||||||
style.TabRounding = 4.0f;
|
|
||||||
style.WindowBorderSize = 1.0f;
|
|
||||||
style.FrameBorderSize = 1.0f;
|
|
||||||
|
|
||||||
ImVec4 *colors = style.Colors;
|
|
||||||
|
|
||||||
colors[ImGuiCol_Text] = nord4; // primary text
|
// Include individual theme implementations split under ./themes
|
||||||
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
|
#include "themes/Nord.h"
|
||||||
colors[ImGuiCol_WindowBg] = nord10;
|
#include "themes/Plan9.h"
|
||||||
colors[ImGuiCol_ChildBg] = nord0;
|
#include "themes/Solarized.h"
|
||||||
colors[ImGuiCol_PopupBg] = RGBA(0x2E3440, 0.98f);
|
#include "themes/Gruvbox.h"
|
||||||
colors[ImGuiCol_Border] = nord1;
|
#include "themes/EInk.h"
|
||||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
|
||||||
|
|
||||||
colors[ImGuiCol_FrameBg] = nord2;
|
|
||||||
colors[ImGuiCol_FrameBgHovered] = nord3;
|
|
||||||
colors[ImGuiCol_FrameBgActive] = nord10;
|
|
||||||
|
|
||||||
colors[ImGuiCol_TitleBg] = nord1;
|
// Theme abstraction and registry (generalized theme system)
|
||||||
colors[ImGuiCol_TitleBgActive] = nord3;
|
class Theme {
|
||||||
colors[ImGuiCol_TitleBgCollapsed] = nord1;
|
public:
|
||||||
|
virtual ~Theme() = default;
|
||||||
|
|
||||||
colors[ImGuiCol_MenuBarBg] = nord1;
|
[[nodiscard]] virtual const char *Name() const = 0; // canonical name (e.g., "nord", "gruvbox-dark")
|
||||||
colors[ImGuiCol_ScrollbarBg] = nord0;
|
virtual void Apply() const = 0; // apply to current ImGui style
|
||||||
colors[ImGuiCol_ScrollbarGrab] = nord3;
|
virtual ThemeId Id() = 0; // theme identifier
|
||||||
colors[ImGuiCol_ScrollbarGrabHovered] = nord10;
|
};
|
||||||
colors[ImGuiCol_ScrollbarGrabActive] = nord9;
|
|
||||||
|
|
||||||
colors[ImGuiCol_CheckMark] = nord8;
|
namespace detail {
|
||||||
colors[ImGuiCol_SliderGrab] = nord8;
|
struct NordTheme final : Theme {
|
||||||
colors[ImGuiCol_SliderGrabActive] = nord9;
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "nord";
|
||||||
|
}
|
||||||
|
|
||||||
colors[ImGuiCol_Button] = nord3;
|
|
||||||
colors[ImGuiCol_ButtonHovered] = nord10;
|
|
||||||
colors[ImGuiCol_ButtonActive] = nord9;
|
|
||||||
|
|
||||||
colors[ImGuiCol_Header] = nord3;
|
void Apply() const override
|
||||||
colors[ImGuiCol_HeaderHovered] = nord10;
|
{
|
||||||
colors[ImGuiCol_HeaderActive] = nord10;
|
ApplyNordImGuiTheme();
|
||||||
|
}
|
||||||
|
|
||||||
colors[ImGuiCol_Separator] = nord2;
|
|
||||||
colors[ImGuiCol_SeparatorHovered] = nord10;
|
|
||||||
colors[ImGuiCol_SeparatorActive] = nord9;
|
|
||||||
|
|
||||||
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
|
ThemeId Id() override
|
||||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
|
{
|
||||||
colors[ImGuiCol_ResizeGripActive] = nord9;
|
return ThemeId::Nord;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
colors[ImGuiCol_Tab] = nord2;
|
struct GruvboxTheme final : Theme {
|
||||||
colors[ImGuiCol_TabHovered] = nord10;
|
[[nodiscard]] const char *Name() const override
|
||||||
colors[ImGuiCol_TabActive] = nord3;
|
{
|
||||||
colors[ImGuiCol_TabUnfocused] = nord2;
|
return "gruvbox";
|
||||||
colors[ImGuiCol_TabUnfocusedActive] = nord3;
|
}
|
||||||
|
|
||||||
// Docking colors are available only when docking branch is enabled; omit for compatibility
|
|
||||||
|
|
||||||
colors[ImGuiCol_TableHeaderBg] = nord2;
|
void Apply() const override
|
||||||
colors[ImGuiCol_TableBorderStrong] = nord1;
|
{
|
||||||
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
|
if (gBackgroundMode == BackgroundMode::Light)
|
||||||
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
|
ApplyGruvboxLightMediumTheme();
|
||||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
|
else
|
||||||
|
ApplyGruvboxDarkMediumTheme();
|
||||||
|
}
|
||||||
|
|
||||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
|
|
||||||
colors[ImGuiCol_DragDropTarget] = nord13;
|
|
||||||
colors[ImGuiCol_NavHighlight] = nord9;
|
|
||||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
|
|
||||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
|
||||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
|
||||||
|
|
||||||
// Plots
|
ThemeId Id() override
|
||||||
colors[ImGuiCol_PlotLines] = nord8;
|
{
|
||||||
colors[ImGuiCol_PlotLinesHovered] = nord9;
|
// Legacy maps to dark; unified under base id GruvboxDarkMedium
|
||||||
colors[ImGuiCol_PlotHistogram] = nord13;
|
return ThemeId::GruvboxDarkMedium;
|
||||||
colors[ImGuiCol_PlotHistogramHovered] = nord12;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EInkTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "eink";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Dark)
|
||||||
|
ApplyEInkDarkImGuiTheme();
|
||||||
|
else
|
||||||
|
ApplyEInkImGuiTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::EInk;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SolarizedTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "solarized";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Light)
|
||||||
|
ApplySolarizedLightTheme();
|
||||||
|
else
|
||||||
|
ApplySolarizedDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Solarized;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Plan9Theme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "plan9";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
ApplyPlan9Theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Plan9;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
static const std::vector<std::unique_ptr<Theme> > &
|
||||||
|
ThemeRegistry()
|
||||||
|
{
|
||||||
|
static std::vector<std::unique_ptr<Theme> > reg;
|
||||||
|
if (reg.empty()) {
|
||||||
|
// Alphabetical by canonical name: eink, gruvbox, nord, plan9, solarized
|
||||||
|
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||||
|
}
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Canonical theme name for a given ThemeId (via registry order)
|
||||||
|
[[maybe_unused]] static const char *
|
||||||
|
ThemeName(const ThemeId id)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
const size_t idx = ThemeIndexFromId(id);
|
||||||
|
if (idx < reg.size())
|
||||||
|
return reg[idx]->Name();
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper to apply a theme by id and update current theme
|
||||||
|
static void
|
||||||
|
ApplyTheme(const ThemeId id)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
const size_t idx = ThemeIndexFromId(id);
|
||||||
|
if (idx < reg.size()) {
|
||||||
|
reg[idx]->Apply();
|
||||||
|
gCurrentTheme = id;
|
||||||
|
gCurrentThemeIndex = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
CurrentTheme()
|
||||||
|
{
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Cycle helpers
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
NextTheme()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (reg.empty()) {
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t nxt = (gCurrentThemeIndex + 1) % reg.size();
|
||||||
|
ApplyTheme(ThemeIdFromIndex(nxt));
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
PrevTheme()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (reg.empty()) {
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t prv = (gCurrentThemeIndex + reg.size() - 1) % reg.size();
|
||||||
|
ApplyTheme(ThemeIdFromIndex(prv));
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Name-based API
|
||||||
|
[[maybe_unused]] static const Theme *
|
||||||
|
GetThemeByName(const std::string &name)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
for (const auto &t: reg) {
|
||||||
|
if (name == t->Name())
|
||||||
|
return t.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static bool
|
||||||
|
ApplyThemeByName(const std::string &name)
|
||||||
|
{
|
||||||
|
// Handle aliases and background-specific names
|
||||||
|
std::string n = name;
|
||||||
|
// lowercase copy
|
||||||
|
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (n == "gruvbox-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "gruvbox";
|
||||||
|
} else if (n == "gruvbox-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "gruvbox";
|
||||||
|
} else if (n == "solarized-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "solarized";
|
||||||
|
} else if (n == "solarized-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "solarized";
|
||||||
|
} else if (n == "eink-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "eink";
|
||||||
|
} else if (n == "eink-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "eink";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
for (size_t i = 0; i < reg.size(); ++i) {
|
||||||
|
if (n == reg[i]->Name()) {
|
||||||
|
reg[i]->Apply();
|
||||||
|
gCurrentThemeIndex = i;
|
||||||
|
gCurrentTheme = ThemeIdFromIndex(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static const char *
|
||||||
|
CurrentThemeName()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (gCurrentThemeIndex < reg.size()) {
|
||||||
|
return reg[gCurrentThemeIndex]->Name();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helpers to map between legacy ThemeId and registry index
|
||||||
|
static size_t
|
||||||
|
ThemeIndexFromId(const ThemeId id)
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case ThemeId::EInk:
|
||||||
|
return 0;
|
||||||
|
case ThemeId::GruvboxDarkMedium:
|
||||||
|
return 1;
|
||||||
|
case ThemeId::Nord:
|
||||||
|
return 2;
|
||||||
|
case ThemeId::Plan9:
|
||||||
|
return 3;
|
||||||
|
case ThemeId::Solarized:
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static ThemeId
|
||||||
|
ThemeIdFromIndex(const size_t idx)
|
||||||
|
{
|
||||||
|
switch (idx) {
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
return ThemeId::EInk;
|
||||||
|
case 1:
|
||||||
|
return ThemeId::GruvboxDarkMedium; // unified gruvbox
|
||||||
|
case 2:
|
||||||
|
return ThemeId::Nord;
|
||||||
|
case 3:
|
||||||
|
return ThemeId::Plan9;
|
||||||
|
case 4:
|
||||||
|
return ThemeId::Solarized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||||
|
[[maybe_unused]] static ImVec4
|
||||||
|
SyntaxInk(const TokenKind k)
|
||||||
|
{
|
||||||
|
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
||||||
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
|
// Base text
|
||||||
|
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||||
|
case TokenKind::String:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Char:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||||
|
case TokenKind::Number:
|
||||||
|
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
|
||||||
|
case TokenKind::Function:
|
||||||
|
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
|
||||||
|
case TokenKind::Operator:
|
||||||
|
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Punctuation:
|
||||||
|
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Identifier:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Whitespace:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
|
||||||
|
case TokenKind::Default: default:
|
||||||
|
return def;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
|
|||||||
50
HelpText.cc
50
HelpText.cc
@@ -15,24 +15,26 @@ HelpText::Text()
|
|||||||
return std::string(
|
return std::string(
|
||||||
"KTE - Kyle's Text Editor\n\n"
|
"KTE - Kyle's Text Editor\n\n"
|
||||||
"About:\n"
|
"About:\n"
|
||||||
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
|
" kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
|
||||||
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
|
" WordStar/VDE-style command model with some emacs influences.\n"
|
||||||
" process of writing a text editor from scratch. It has keybindings inspired by\n"
|
|
||||||
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Core keybindings:\n"
|
"K-commands (prefix C-k):\n"
|
||||||
" C-k ' Toggle read-only\n"
|
" C-k ' Toggle read-only\n"
|
||||||
" C-k - Unindent region\n"
|
" C-k - Unindent region (mark required)\n"
|
||||||
" C-k = Indent region\n"
|
" C-k = Indent region (mark required)\n"
|
||||||
|
" C-k ; Command prompt (:\\ )\n"
|
||||||
" C-k C-d Kill entire line\n"
|
" C-k C-d Kill entire line\n"
|
||||||
" C-k C-q Quit now (no confirm)\n"
|
" C-k C-q Quit now (no confirm)\n"
|
||||||
" C-k a Mark all and jump to end\n"
|
" C-k C-x Save and quit\n"
|
||||||
|
" C-k a Mark start of file, jump to end\n"
|
||||||
" C-k b Switch buffer\n"
|
" C-k b Switch buffer\n"
|
||||||
" C-k c Close current buffer\n"
|
" C-k c Close current buffer\n"
|
||||||
" C-k d Kill to end of line\n"
|
" C-k d Kill to end of line\n"
|
||||||
" C-k e Open file (prompt)\n"
|
" C-k e Open file (prompt)\n"
|
||||||
|
" C-k f Flush kill ring\n"
|
||||||
" C-k g Jump to line\n"
|
" C-k g Jump to line\n"
|
||||||
" C-k h Show this help\n"
|
" C-k h Show this help\n"
|
||||||
|
" C-k j Jump to mark\n"
|
||||||
" C-k l Reload buffer from disk\n"
|
" C-k l Reload buffer from disk\n"
|
||||||
" C-k n Previous buffer\n"
|
" C-k n Previous buffer\n"
|
||||||
" C-k o Change working directory (prompt)\n"
|
" C-k o Change working directory (prompt)\n"
|
||||||
@@ -44,12 +46,36 @@ HelpText::Text()
|
|||||||
" C-k v Toggle visual file picker (GUI)\n"
|
" C-k v Toggle visual file picker (GUI)\n"
|
||||||
" C-k w Show working directory\n"
|
" C-k w Show working directory\n"
|
||||||
" C-k x Save and quit\n"
|
" C-k x Save and quit\n"
|
||||||
|
" C-k y Yank\n"
|
||||||
"\n"
|
"\n"
|
||||||
"ESC/Alt commands:\n"
|
"ESC/Alt commands:\n"
|
||||||
|
" ESC < Go to beginning of file\n"
|
||||||
|
" ESC > Go to end of file\n"
|
||||||
|
" ESC m Toggle mark\n"
|
||||||
|
" ESC w Copy region to kill ring (Alt-w)\n"
|
||||||
|
" ESC b Previous word\n"
|
||||||
|
" ESC f Next word\n"
|
||||||
|
" ESC d Delete next word (Alt-d)\n"
|
||||||
|
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||||
" ESC q Reflow paragraph\n"
|
" ESC q Reflow paragraph\n"
|
||||||
" ESC BACKSPACE Delete previous word\n"
|
"\n"
|
||||||
" ESC d Delete next word\n"
|
"Control keys:\n"
|
||||||
" Alt-w Copy region to kill ring\n\n"
|
" C-a C-e Line start / end\n"
|
||||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"
|
" C-b C-f Move left / right\n"
|
||||||
|
" C-n C-p Move down / up\n"
|
||||||
|
" C-d Delete char\n"
|
||||||
|
" C-w / C-y Kill region / Yank\n"
|
||||||
|
" C-s Incremental find\n"
|
||||||
|
" C-r Regex search\n"
|
||||||
|
" C-t Regex search & replace\n"
|
||||||
|
" C-h Search & replace\n"
|
||||||
|
" C-l / C-g Refresh / Cancel\n"
|
||||||
|
" C-u [digits] Universal argument (repeat count)\n"
|
||||||
|
"\n"
|
||||||
|
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||||
|
"\n"
|
||||||
|
"GUI appearance (command prompt):\n"
|
||||||
|
" : theme NAME Set GUI theme (eink, gruvbox, nord, plan9, solarized)\n"
|
||||||
|
" : background MODE Set background: light | dark (affects eink, gruvbox, solarized)\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
Highlight.h
Normal file
37
Highlight.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Highlight.h - core syntax highlighting types for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Token kinds shared between renderers and highlighters
|
||||||
|
enum class TokenKind {
|
||||||
|
Default,
|
||||||
|
Keyword,
|
||||||
|
Type,
|
||||||
|
String,
|
||||||
|
Char,
|
||||||
|
Comment,
|
||||||
|
Number,
|
||||||
|
Preproc,
|
||||||
|
Constant,
|
||||||
|
Function,
|
||||||
|
Operator,
|
||||||
|
Punctuation,
|
||||||
|
Identifier,
|
||||||
|
Whitespace,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HighlightSpan {
|
||||||
|
int col_start{0}; // inclusive, 0-based columns in buffer indices
|
||||||
|
int col_end{0}; // exclusive
|
||||||
|
TokenKind kind{TokenKind::Default};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LineHighlight {
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
std::uint64_t version{0}; // buffer version used for this line
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
@@ -108,6 +108,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case '=':
|
case '=':
|
||||||
out = CommandId::IndentRegion;
|
out = CommandId::IndentRegion;
|
||||||
return true;
|
return true;
|
||||||
|
case ';':
|
||||||
|
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ ROADMAP / TODO:
|
|||||||
|
|
||||||
- [x] Search + Replace
|
- [x] Search + Replace
|
||||||
- [x] Regex search + replace
|
- [x] Regex search + replace
|
||||||
- [ ] The undo system should actually work
|
|
||||||
- [x] Able to mark buffers as read-only
|
- [x] Able to mark buffers as read-only
|
||||||
- [x] Built-in help text
|
- [x] Built-in help text
|
||||||
- [x] Shorten paths in the homedir with ~
|
- [x] Shorten paths in the homedir with ~
|
||||||
- [x] When the filename is longer than the message window, scoot left to
|
- [x] When the filename is longer than the message window, scoot left to
|
||||||
keep it in view
|
keep it in view
|
||||||
|
- [x] Syntax highlighting
|
||||||
|
- [ ] The undo system should actually work
|
||||||
|
- [ ] LSP integration
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "TerminalRenderer.h"
|
#include "TerminalRenderer.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
|
||||||
// Version string expected to be provided by build system as KTE_VERSION_STR
|
// Version string expected to be provided by build system as KTE_VERSION_STR
|
||||||
#ifndef KTE_VERSION_STR
|
#ifndef KTE_VERSION_STR
|
||||||
@@ -41,6 +42,13 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::size_t coloffs = buf->Coloffs();
|
std::size_t coloffs = buf->Coloffs();
|
||||||
|
|
||||||
const int tabw = 8;
|
const int tabw = 8;
|
||||||
|
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||||
|
int fr = static_cast<int>(rowoffs);
|
||||||
|
int rc = std::max(0, content_rows);
|
||||||
|
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||||
|
}
|
||||||
|
|
||||||
for (int r = 0; r < content_rows; ++r) {
|
for (int r = 0; r < content_rows; ++r) {
|
||||||
move(r, 0);
|
move(r, 0);
|
||||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||||
@@ -52,7 +60,9 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
if (search_mode && li < lines.size()) {
|
if (search_mode && li < lines.size()) {
|
||||||
std::string sline = static_cast<std::string>(lines[li]);
|
std::string sline = static_cast<std::string>(lines[li]);
|
||||||
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
||||||
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
if (ed.PromptActive() && (
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
try {
|
try {
|
||||||
std::regex rx(ed.SearchQuery());
|
std::regex rx(ed.SearchQuery());
|
||||||
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||||
@@ -75,12 +85,15 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
||||||
if (ranges.empty()) return false;
|
if (ranges.empty())
|
||||||
|
return false;
|
||||||
// ranges are non-overlapping and ordered by construction
|
// ranges are non-overlapping and ordered by construction
|
||||||
// linear scan is fine for now
|
// linear scan is fine for now
|
||||||
for (const auto &rg: ranges) {
|
for (const auto &rg: ranges) {
|
||||||
if (si < rg.first) break;
|
if (si < rg.first)
|
||||||
if (si >= rg.first && si < rg.second) return true;
|
break;
|
||||||
|
if (si >= rg.first && si < rg.second)
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -96,6 +109,46 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::string line = static_cast<std::string>(lines[li]);
|
std::string line = static_cast<std::string>(lines[li]);
|
||||||
src_i = 0;
|
src_i = 0;
|
||||||
render_col = 0;
|
render_col = 0;
|
||||||
|
// Syntax highlighting: fetch per-line spans
|
||||||
|
const kte::LineHighlight *lh_ptr = nullptr;
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||||
|
HasHighlighter()) {
|
||||||
|
lh_ptr = &buf->Highlighter()->GetLine(
|
||||||
|
*buf, static_cast<int>(li), buf->Version());
|
||||||
|
}
|
||||||
|
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||||
|
if (!lh_ptr)
|
||||||
|
return kte::TokenKind::Default;
|
||||||
|
for (const auto &sp: lh_ptr->spans) {
|
||||||
|
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
|
||||||
|
src_index) < sp.col_end)
|
||||||
|
return sp.kind;
|
||||||
|
}
|
||||||
|
return kte::TokenKind::Default;
|
||||||
|
};
|
||||||
|
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||||
|
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||||
|
attrset(A_NORMAL);
|
||||||
|
switch (k) {
|
||||||
|
case kte::TokenKind::Keyword:
|
||||||
|
case kte::TokenKind::Type:
|
||||||
|
case kte::TokenKind::Constant:
|
||||||
|
case kte::TokenKind::Function:
|
||||||
|
attron(A_BOLD);
|
||||||
|
break;
|
||||||
|
case kte::TokenKind::Comment:
|
||||||
|
attron(A_DIM);
|
||||||
|
break;
|
||||||
|
case kte::TokenKind::String:
|
||||||
|
case kte::TokenKind::Char:
|
||||||
|
case kte::TokenKind::Number:
|
||||||
|
// standout a bit using A_UNDERLINE if available
|
||||||
|
attron(A_UNDERLINE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
while (written < cols) {
|
while (written < cols) {
|
||||||
char ch = ' ';
|
char ch = ' ';
|
||||||
bool from_src = false;
|
bool from_src = false;
|
||||||
@@ -119,15 +172,35 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
// Now render visible spaces
|
// Now render visible spaces
|
||||||
while (next_tab > 0 && written < cols) {
|
while (next_tab > 0 && written < cols) {
|
||||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||||
bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
|
bool in_cur =
|
||||||
|
has_current && li == cur_my && src_i >= cur_mx
|
||||||
|
&& src_i < cur_mend;
|
||||||
// Toggle highlight attributes
|
// Toggle highlight attributes
|
||||||
int attr = 0;
|
int attr = 0;
|
||||||
if (in_hl) attr |= A_STANDOUT;
|
if (in_hl)
|
||||||
if (in_cur) attr |= A_BOLD;
|
attr |= A_STANDOUT;
|
||||||
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
|
if (in_cur)
|
||||||
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
|
attr |= A_BOLD;
|
||||||
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
|
if ((attr & A_STANDOUT) && !hl_on) {
|
||||||
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
|
attron(A_STANDOUT);
|
||||||
|
hl_on = true;
|
||||||
|
}
|
||||||
|
if (!(attr & A_STANDOUT) && hl_on) {
|
||||||
|
attroff(A_STANDOUT);
|
||||||
|
hl_on = false;
|
||||||
|
}
|
||||||
|
if ((attr & A_BOLD) && !cur_on) {
|
||||||
|
attron(A_BOLD);
|
||||||
|
cur_on = true;
|
||||||
|
}
|
||||||
|
if (!(attr & A_BOLD) && cur_on) {
|
||||||
|
attroff(A_BOLD);
|
||||||
|
cur_on = false;
|
||||||
|
}
|
||||||
|
// Apply syntax attribute only if not in search highlight
|
||||||
|
if (!in_hl) {
|
||||||
|
apply_token_attr(token_at(src_i));
|
||||||
|
}
|
||||||
addch(' ');
|
addch(' ');
|
||||||
++written;
|
++written;
|
||||||
++render_col;
|
++render_col;
|
||||||
@@ -151,11 +224,28 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
from_src = false;
|
from_src = false;
|
||||||
}
|
}
|
||||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||||
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
|
bool in_cur =
|
||||||
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
|
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||||
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
|
cur_mend;
|
||||||
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
|
if (in_hl && !hl_on) {
|
||||||
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
|
attron(A_STANDOUT);
|
||||||
|
hl_on = true;
|
||||||
|
}
|
||||||
|
if (!in_hl && hl_on) {
|
||||||
|
attroff(A_STANDOUT);
|
||||||
|
hl_on = false;
|
||||||
|
}
|
||||||
|
if (in_cur && !cur_on) {
|
||||||
|
attron(A_BOLD);
|
||||||
|
cur_on = true;
|
||||||
|
}
|
||||||
|
if (!in_cur && cur_on) {
|
||||||
|
attroff(A_BOLD);
|
||||||
|
cur_on = false;
|
||||||
|
}
|
||||||
|
if (!in_hl && from_src) {
|
||||||
|
apply_token_attr(token_at(src_i));
|
||||||
|
}
|
||||||
addch(static_cast<unsigned char>(ch));
|
addch(static_cast<unsigned char>(ch));
|
||||||
++written;
|
++written;
|
||||||
++render_col;
|
++render_col;
|
||||||
@@ -173,6 +263,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
attroff(A_BOLD);
|
attroff(A_BOLD);
|
||||||
cur_on = false;
|
cur_on = false;
|
||||||
}
|
}
|
||||||
|
attrset(A_NORMAL);
|
||||||
clrtoeol();
|
clrtoeol();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,11 +313,14 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
// Prefer keeping the tail of the filename visible when it exceeds the window
|
// Prefer keeping the tail of the filename visible when it exceeds the window
|
||||||
std::string msg;
|
std::string msg;
|
||||||
if (!label.empty()) {
|
if (kind == Editor::PromptKind::Command) {
|
||||||
|
msg = ": ";
|
||||||
|
} else if (!label.empty()) {
|
||||||
msg = label + ": ";
|
msg = label + ": ";
|
||||||
}
|
}
|
||||||
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
||||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
|
Editor::PromptKind::Chdir) && cols > 0) {
|
||||||
int avail = cols - static_cast<int>(msg.size());
|
int avail = cols - static_cast<int>(msg.size());
|
||||||
if (avail <= 0) {
|
if (avail <= 0) {
|
||||||
// No room for label; fall back to showing the rightmost portion of the whole string
|
// No room for label; fall back to showing the rightmost portion of the whole string
|
||||||
|
|||||||
@@ -338,31 +338,42 @@ UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
|||||||
buf_ = &new_buf;
|
buf_ = &new_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---- Debug helpers ----
|
// ---- Debug helpers ----
|
||||||
const char *
|
const char *
|
||||||
UndoSystem::type_str(UndoType t)
|
UndoSystem::type_str(UndoType t)
|
||||||
{
|
{
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case UndoType::Insert: return "Insert";
|
case UndoType::Insert:
|
||||||
case UndoType::Delete: return "Delete";
|
return "Insert";
|
||||||
case UndoType::Paste: return "Paste";
|
case UndoType::Delete:
|
||||||
case UndoType::Newline: return "Newline";
|
return "Delete";
|
||||||
case UndoType::DeleteRow: return "DeleteRow";
|
case UndoType::Paste:
|
||||||
|
return "Paste";
|
||||||
|
case UndoType::Newline:
|
||||||
|
return "Newline";
|
||||||
|
case UndoType::DeleteRow:
|
||||||
|
return "DeleteRow";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
||||||
{
|
{
|
||||||
if (!root || !target) return false;
|
if (!root || !target)
|
||||||
if (root == target) return true;
|
return false;
|
||||||
|
if (root == target)
|
||||||
|
return true;
|
||||||
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
||||||
if (is_descendant(child, target)) return true;
|
if (is_descendant(child, target))
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::debug_log(const char *op) const
|
UndoSystem::debug_log(const char *op) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ private:
|
|||||||
|
|
||||||
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
||||||
void debug_log(const char *op) const;
|
void debug_log(const char *op) const;
|
||||||
|
|
||||||
static const char *type_str(UndoType t);
|
static const char *type_str(UndoType t);
|
||||||
|
|
||||||
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
||||||
|
|
||||||
void update_dirty_flag();
|
void update_dirty_flag();
|
||||||
|
|||||||
74
docs/kge.1
74
docs/kge.1
@@ -1,7 +1,7 @@
|
|||||||
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
|
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
|
||||||
.\"
|
.\"
|
||||||
.\" Project homepage: https://github.com/wntrmute/kte
|
.\" Project homepage: https://github.com/wntrmute/kte
|
||||||
.TH KGE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
.TH KGE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
kge \- Kyle's Graphical Editor (GUI-first)
|
kge \- Kyle's Graphical Editor (GUI-first)
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
|
|||||||
.PP
|
.PP
|
||||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||||
.TP
|
.TP
|
||||||
.B C-k BACKSPACE
|
.B C-k '
|
||||||
Delete from the cursor to the beginning of the line.
|
Toggle read-only for the current buffer.
|
||||||
.TP
|
|
||||||
.B C-k SPACE
|
|
||||||
Toggle the mark.
|
|
||||||
.TP
|
.TP
|
||||||
.B C-k -
|
.B C-k -
|
||||||
If the mark is set, unindent the region.
|
If the mark is set, unindent the region.
|
||||||
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
|
|||||||
.B C-k =
|
.B C-k =
|
||||||
If the mark is set, indent the region.
|
If the mark is set, indent the region.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k ;
|
||||||
|
Open the generic command prompt (": ").
|
||||||
|
.TP
|
||||||
.B C-k a
|
.B C-k a
|
||||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||||
.TP
|
.TP
|
||||||
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
|
|||||||
Delete the entire line.
|
Delete the entire line.
|
||||||
.TP
|
.TP
|
||||||
.B C-k e
|
.B C-k e
|
||||||
Edit a new file.
|
Edit (open) a new file.
|
||||||
.TP
|
.TP
|
||||||
.B C-k f
|
.B C-k f
|
||||||
Flush the kill ring.
|
Flush the kill ring.
|
||||||
@@ -88,14 +88,20 @@ Flush the kill ring.
|
|||||||
.B C-k g
|
.B C-k g
|
||||||
Go to a specific line.
|
Go to a specific line.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k h
|
||||||
|
Show the built-in help (+HELP+ buffer).
|
||||||
|
.TP
|
||||||
.B C-k j
|
.B C-k j
|
||||||
Jump to the mark.
|
Jump to the mark.
|
||||||
.TP
|
.TP
|
||||||
.B C-k l
|
.B C-k l
|
||||||
Reload the current buffer from disk.
|
Reload the current buffer from disk.
|
||||||
.TP
|
.TP
|
||||||
.B C-k m
|
.B C-k n
|
||||||
Run make(1), reporting success or failure.
|
Switch to the previous buffer.
|
||||||
|
.TP
|
||||||
|
.B C-k o
|
||||||
|
Change working directory (prompt).
|
||||||
.TP
|
.TP
|
||||||
.B C-k p
|
.B C-k p
|
||||||
Switch to the next buffer.
|
Switch to the next buffer.
|
||||||
@@ -106,14 +112,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
|||||||
.B C-k C-q
|
.B C-k C-q
|
||||||
Immediately exit the editor.
|
Immediately exit the editor.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k r
|
||||||
|
Redo changes.
|
||||||
|
.TP
|
||||||
.B C-k s
|
.B C-k s
|
||||||
Save the file, prompting for a filename if needed.
|
Save the file, prompting for a filename if needed.
|
||||||
.TP
|
.TP
|
||||||
.B C-k u
|
.B C-k u
|
||||||
Undo.
|
Undo.
|
||||||
.TP
|
.TP
|
||||||
.B C-k r
|
.B C-k v
|
||||||
Redo changes.
|
Toggle visual file picker (GUI).
|
||||||
|
.TP
|
||||||
|
.B C-k w
|
||||||
|
Show the current working directory.
|
||||||
.TP
|
.TP
|
||||||
.B C-k x
|
.B C-k x
|
||||||
Save the file and exit. Also C-k C-x.
|
Save the file and exit. Also C-k C-x.
|
||||||
@@ -121,23 +133,50 @@ Save the file and exit. Also C-k C-x.
|
|||||||
.B C-k y
|
.B C-k y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
.B C-k \e
|
.B C-k C-x
|
||||||
Dump core.
|
Save the file and exit.
|
||||||
|
|
||||||
.SS Other keybindings
|
.SS Other keybindings
|
||||||
.TP
|
.TP
|
||||||
.B C-g
|
.B C-g
|
||||||
Cancel the current operation.
|
Cancel the current operation.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-a
|
||||||
|
Move to the beginning of the line.
|
||||||
|
.TP
|
||||||
|
.B C-e
|
||||||
|
Move to the end of the line.
|
||||||
|
.TP
|
||||||
|
.B C-b
|
||||||
|
Move left.
|
||||||
|
.TP
|
||||||
|
.B C-f
|
||||||
|
Move right.
|
||||||
|
.TP
|
||||||
|
.B C-n
|
||||||
|
Move down.
|
||||||
|
.TP
|
||||||
|
.B C-p
|
||||||
|
Move up.
|
||||||
|
.TP
|
||||||
.B C-l
|
.B C-l
|
||||||
Refresh the display.
|
Refresh the display.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-d
|
||||||
|
Delete the character at the cursor.
|
||||||
|
.TP
|
||||||
.B C-r
|
.B C-r
|
||||||
Regex search.
|
Regex search.
|
||||||
.TP
|
.TP
|
||||||
.B C-s
|
.B C-s
|
||||||
Incremental find.
|
Incremental find.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-t
|
||||||
|
Regex search and replace.
|
||||||
|
.TP
|
||||||
|
.B C-h
|
||||||
|
Search and replace.
|
||||||
|
.TP
|
||||||
.B C-u
|
.B C-u
|
||||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||||
.TP
|
.TP
|
||||||
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
|
|||||||
.B C-y
|
.B C-y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
|
.B ESC <
|
||||||
|
Move to the beginning of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC >
|
||||||
|
Move to the end of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC m
|
||||||
|
Toggle the mark.
|
||||||
|
.TP
|
||||||
.B ESC BACKSPACE
|
.B ESC BACKSPACE
|
||||||
Delete the previous word.
|
Delete the previous word.
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
100
docs/kte.1
100
docs/kte.1
@@ -1,7 +1,7 @@
|
|||||||
.\" kte(1) — Kyle's Text Editor (terminal-first)
|
.\" kte(1) — Kyle's Text Editor (terminal-first)
|
||||||
.\"
|
.\"
|
||||||
.\" Project homepage: https://github.com/wntrmute/kte
|
.\" Project homepage: https://github.com/wntrmute/kte
|
||||||
.TH KTE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
.TH KTE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
kte \- Kyle's Text Editor (terminal-first)
|
kte \- Kyle's Text Editor (terminal-first)
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
|
|||||||
.PP
|
.PP
|
||||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||||
.TP
|
.TP
|
||||||
.B C-k BACKSPACE
|
.B C-k '
|
||||||
Delete from the cursor to the beginning of the line.
|
Toggle read-only for the current buffer.
|
||||||
.TP
|
|
||||||
.B C-k SPACE
|
|
||||||
Toggle the mark.
|
|
||||||
.TP
|
.TP
|
||||||
.B C-k -
|
.B C-k -
|
||||||
If the mark is set, unindent the region.
|
If the mark is set, unindent the region.
|
||||||
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
|
|||||||
.B C-k =
|
.B C-k =
|
||||||
If the mark is set, indent the region.
|
If the mark is set, indent the region.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k ;
|
||||||
|
Open the generic command prompt (": ").
|
||||||
|
.TP
|
||||||
.B C-k a
|
.B C-k a
|
||||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||||
.TP
|
.TP
|
||||||
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
|
|||||||
Delete the entire line.
|
Delete the entire line.
|
||||||
.TP
|
.TP
|
||||||
.B C-k e
|
.B C-k e
|
||||||
Edit a new file.
|
Edit (open) a new file.
|
||||||
.TP
|
.TP
|
||||||
.B C-k f
|
.B C-k f
|
||||||
Flush the kill ring.
|
Flush the kill ring.
|
||||||
@@ -93,14 +93,20 @@ Flush the kill ring.
|
|||||||
.B C-k g
|
.B C-k g
|
||||||
Go to a specific line.
|
Go to a specific line.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k h
|
||||||
|
Show the built-in help (+HELP+ buffer).
|
||||||
|
.TP
|
||||||
.B C-k j
|
.B C-k j
|
||||||
Jump to the mark.
|
Jump to the mark.
|
||||||
.TP
|
.TP
|
||||||
.B C-k l
|
.B C-k l
|
||||||
Reload the current buffer from disk.
|
Reload the current buffer from disk.
|
||||||
.TP
|
.TP
|
||||||
.B C-k m
|
.B C-k n
|
||||||
Run make(1), reporting success or failure.
|
Switch to the previous buffer.
|
||||||
|
.TP
|
||||||
|
.B C-k o
|
||||||
|
Change working directory (prompt).
|
||||||
.TP
|
.TP
|
||||||
.B C-k p
|
.B C-k p
|
||||||
Switch to the next buffer.
|
Switch to the next buffer.
|
||||||
@@ -111,14 +117,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
|||||||
.B C-k C-q
|
.B C-k C-q
|
||||||
Immediately exit the editor.
|
Immediately exit the editor.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k r
|
||||||
|
Redo changes.
|
||||||
|
.TP
|
||||||
.B C-k s
|
.B C-k s
|
||||||
Save the file, prompting for a filename if needed.
|
Save the file, prompting for a filename if needed.
|
||||||
.TP
|
.TP
|
||||||
.B C-k u
|
.B C-k u
|
||||||
Undo.
|
Undo.
|
||||||
.TP
|
.TP
|
||||||
.B C-k r
|
.B C-k v
|
||||||
Redo changes.
|
Toggle visual file picker (GUI).
|
||||||
|
.TP
|
||||||
|
.B C-k w
|
||||||
|
Show the current working directory.
|
||||||
.TP
|
.TP
|
||||||
.B C-k x
|
.B C-k x
|
||||||
Save the file and exit. Also C-k C-x.
|
Save the file and exit. Also C-k C-x.
|
||||||
@@ -126,23 +138,76 @@ Save the file and exit. Also C-k C-x.
|
|||||||
.B C-k y
|
.B C-k y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
.B C-k \e
|
.B C-k C-x
|
||||||
Dump core.
|
Save the file and exit.
|
||||||
|
|
||||||
|
.SH GUI APPEARANCE
|
||||||
|
When running the GUI frontend, you can control appearance via the generic
|
||||||
|
command prompt (type "C-k ;" then enter commands):
|
||||||
|
.TP
|
||||||
|
.B : theme NAME
|
||||||
|
Set the GUI theme. Available names: "nord", "gruvbox", "plan9", "solarized", "eink".
|
||||||
|
Compatibility aliases are also accepted: "gruvbox-dark", "gruvbox-light",
|
||||||
|
"solarized-dark", "solarized-light", "eink-dark", "eink-light".
|
||||||
|
.TP
|
||||||
|
.B : background MODE
|
||||||
|
Set background mode for supported themes. MODE is either "light" or "dark".
|
||||||
|
Themes that respond to background: eink, gruvbox, solarized. The
|
||||||
|
"nord" and "plan9" themes do not vary with background.
|
||||||
|
|
||||||
|
.SH CONFIGURATION
|
||||||
|
The GUI reads a simple configuration file at
|
||||||
|
~/.config/kte/kge.ini. Recognized keys include:
|
||||||
|
.IP "fullscreen=on|off"
|
||||||
|
.IP "columns=NUM"
|
||||||
|
.IP "rows=NUM"
|
||||||
|
.IP "font_size=NUM"
|
||||||
|
.IP "theme=NAME"
|
||||||
|
.IP "background=light|dark"
|
||||||
|
The theme name accepts the values listed above. The background key controls
|
||||||
|
light/dark variants when the selected theme supports it.
|
||||||
|
|
||||||
.SS Other keybindings
|
.SS Other keybindings
|
||||||
.TP
|
.TP
|
||||||
.B C-g
|
.B C-g
|
||||||
Cancel the current operation.
|
Cancel the current operation.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-a
|
||||||
|
Move to the beginning of the line.
|
||||||
|
.TP
|
||||||
|
.B C-e
|
||||||
|
Move to the end of the line.
|
||||||
|
.TP
|
||||||
|
.B C-b
|
||||||
|
Move left.
|
||||||
|
.TP
|
||||||
|
.B C-f
|
||||||
|
Move right.
|
||||||
|
.TP
|
||||||
|
.B C-n
|
||||||
|
Move down.
|
||||||
|
.TP
|
||||||
|
.B C-p
|
||||||
|
Move up.
|
||||||
|
.TP
|
||||||
.B C-l
|
.B C-l
|
||||||
Refresh the display.
|
Refresh the display.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-d
|
||||||
|
Delete the character at the cursor.
|
||||||
|
.TP
|
||||||
.B C-r
|
.B C-r
|
||||||
Regex search.
|
Regex search.
|
||||||
.TP
|
.TP
|
||||||
.B C-s
|
.B C-s
|
||||||
Incremental find.
|
Incremental find.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-t
|
||||||
|
Regex search and replace.
|
||||||
|
.TP
|
||||||
|
.B C-h
|
||||||
|
Search and replace.
|
||||||
|
.TP
|
||||||
.B C-u
|
.B C-u
|
||||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||||
.TP
|
.TP
|
||||||
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
|
|||||||
.B C-y
|
.B C-y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
|
.B ESC <
|
||||||
|
Move to the beginning of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC >
|
||||||
|
Move to the end of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC m
|
||||||
|
Toggle the mark.
|
||||||
|
.TP
|
||||||
.B ESC BACKSPACE
|
.B ESC BACKSPACE
|
||||||
Delete the previous word.
|
Delete the previous word.
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
525
docs/lsp plan.md
Normal file
525
docs/lsp plan.md
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
# LSP Support Implementation Plan for kte
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This plan outlines a comprehensive approach to integrating Language Server Protocol (LSP) support into kte while
|
||||||
|
respecting its core architectural principles: **frontend/backend separation**, **testability**, and **dual terminal/GUI
|
||||||
|
support**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core Architecture
|
||||||
|
|
||||||
|
### 1.1 LSP Client Module Structure
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// LspClient.h - Core LSP client abstraction
|
||||||
|
class LspClient {
|
||||||
|
public:
|
||||||
|
virtual ~LspClient() = default;
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
virtual bool initialize(const std::string& rootPath) = 0;
|
||||||
|
virtual void shutdown() = 0;
|
||||||
|
|
||||||
|
// Document Synchronization
|
||||||
|
virtual void didOpen(const std::string& uri, const std::string& languageId,
|
||||||
|
int version, const std::string& text) = 0;
|
||||||
|
virtual void didChange(const std::string& uri, int version,
|
||||||
|
const std::vector<TextDocumentContentChangeEvent>& changes) = 0;
|
||||||
|
virtual void didClose(const std::string& uri) = 0;
|
||||||
|
virtual void didSave(const std::string& uri) = 0;
|
||||||
|
|
||||||
|
// Language Features
|
||||||
|
virtual void completion(const std::string& uri, Position pos,
|
||||||
|
CompletionCallback callback) = 0;
|
||||||
|
virtual void hover(const std::string& uri, Position pos,
|
||||||
|
HoverCallback callback) = 0;
|
||||||
|
virtual void definition(const std::string& uri, Position pos,
|
||||||
|
LocationCallback callback) = 0;
|
||||||
|
virtual void references(const std::string& uri, Position pos,
|
||||||
|
LocationsCallback callback) = 0;
|
||||||
|
virtual void diagnostics(DiagnosticsCallback callback) = 0;
|
||||||
|
|
||||||
|
// Process Management
|
||||||
|
virtual bool isRunning() const = 0;
|
||||||
|
virtual std::string getServerName() const = 0;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Process-based LSP Implementation
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// LspProcessClient.h - Manages LSP server subprocess
|
||||||
|
class LspProcessClient : public LspClient {
|
||||||
|
private:
|
||||||
|
std::string serverCommand_;
|
||||||
|
std::vector<std::string> serverArgs_;
|
||||||
|
std::unique_ptr<Process> process_;
|
||||||
|
std::unique_ptr<JsonRpcTransport> transport_;
|
||||||
|
std::unordered_map<int, PendingRequest> pendingRequests_;
|
||||||
|
int nextRequestId_ = 1;
|
||||||
|
|
||||||
|
// Async I/O handling
|
||||||
|
std::thread readerThread_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
LspProcessClient(const std::string& command,
|
||||||
|
const std::vector<std::string>& args);
|
||||||
|
// ... implementation of LspClient interface
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 JSON-RPC Transport Layer
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// JsonRpcTransport.h
|
||||||
|
class JsonRpcTransport {
|
||||||
|
public:
|
||||||
|
// Send a request and get the request ID
|
||||||
|
int sendRequest(const std::string& method, const nlohmann::json& params);
|
||||||
|
|
||||||
|
// Send a notification (no response expected)
|
||||||
|
void sendNotification(const std::string& method, const nlohmann::json& params);
|
||||||
|
|
||||||
|
// Read next message (blocking)
|
||||||
|
std::optional<JsonRpcMessage> readMessage();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void writeMessage(const nlohmann::json& message);
|
||||||
|
std::string readContentLength();
|
||||||
|
|
||||||
|
int fdIn_; // stdin to server
|
||||||
|
int fdOut_; // stdout from server
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Incremental Document Updates
|
||||||
|
|
||||||
|
### 2.1 Change Tracking in Buffer
|
||||||
|
|
||||||
|
The key to efficient LSP integration is tracking changes incrementally. This integrates with the existing `Buffer`
|
||||||
|
class:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// TextDocumentContentChangeEvent.h
|
||||||
|
struct TextDocumentContentChangeEvent {
|
||||||
|
std::optional<Range> range; // If nullopt, entire document changed
|
||||||
|
std::optional<int> rangeLength; // Deprecated but some servers use it
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BufferChangeTracker.h - Integrates with Buffer to track changes
|
||||||
|
class BufferChangeTracker {
|
||||||
|
public:
|
||||||
|
explicit BufferChangeTracker(Buffer* buffer);
|
||||||
|
|
||||||
|
// Called by Buffer on each edit operation
|
||||||
|
void recordInsertion(Position pos, const std::string& text);
|
||||||
|
void recordDeletion(Range range, const std::string& deletedText);
|
||||||
|
|
||||||
|
// Get accumulated changes since last sync
|
||||||
|
std::vector<TextDocumentContentChangeEvent> getChanges();
|
||||||
|
|
||||||
|
// Clear changes after sending to LSP
|
||||||
|
void clearChanges();
|
||||||
|
|
||||||
|
// Get current document version
|
||||||
|
int getVersion() const { return version_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Buffer* buffer_;
|
||||||
|
int version_ = 0;
|
||||||
|
std::vector<TextDocumentContentChangeEvent> pendingChanges_;
|
||||||
|
|
||||||
|
// Optional: Coalesce adjacent changes
|
||||||
|
void coalesceChanges();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Integration with Buffer Operations
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// Buffer.h additions
|
||||||
|
class Buffer {
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
// LSP integration
|
||||||
|
void setChangeTracker(std::unique_ptr<BufferChangeTracker> tracker);
|
||||||
|
BufferChangeTracker* getChangeTracker() { return changeTracker_.get(); }
|
||||||
|
|
||||||
|
// These methods should call tracker when present
|
||||||
|
void insertText(Position pos, const std::string& text);
|
||||||
|
void deleteRange(Range range);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<BufferChangeTracker> changeTracker_;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Sync Strategy Selection
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// LspSyncMode.h
|
||||||
|
enum class LspSyncMode {
|
||||||
|
None, // No sync
|
||||||
|
Full, // Send full document on each change
|
||||||
|
Incremental // Send only changes (preferred)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determined during server capability negotiation
|
||||||
|
LspSyncMode negotiateSyncMode(const ServerCapabilities& caps);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Diagnostics Display System
|
||||||
|
|
||||||
|
### 3.1 Diagnostic Data Model
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// Diagnostic.h
|
||||||
|
enum class DiagnosticSeverity {
|
||||||
|
Error = 1,
|
||||||
|
Warning = 2,
|
||||||
|
Information = 3,
|
||||||
|
Hint = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
Range range;
|
||||||
|
DiagnosticSeverity severity;
|
||||||
|
std::optional<std::string> code;
|
||||||
|
std::optional<std::string> source;
|
||||||
|
std::string message;
|
||||||
|
std::vector<DiagnosticRelatedInformation> relatedInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
// DiagnosticStore.h - Central storage for diagnostics
|
||||||
|
class DiagnosticStore {
|
||||||
|
public:
|
||||||
|
void setDiagnostics(const std::string& uri,
|
||||||
|
std::vector<Diagnostic> diagnostics);
|
||||||
|
const std::vector<Diagnostic>& getDiagnostics(const std::string& uri) const;
|
||||||
|
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string& uri,
|
||||||
|
int line) const;
|
||||||
|
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string& uri,
|
||||||
|
Position pos) const;
|
||||||
|
void clear(const std::string& uri);
|
||||||
|
void clearAll();
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
int getErrorCount(const std::string& uri) const;
|
||||||
|
int getWarningCount(const std::string& uri) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::vector<Diagnostic>> diagnostics_;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Frontend-Agnostic Diagnostic Display Interface
|
||||||
|
|
||||||
|
Following kte's existing abstraction pattern with `Frontend`, `Renderer`, and `InputHandler`:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// DiagnosticDisplay.h - Abstract interface for showing diagnostics
|
||||||
|
class DiagnosticDisplay {
|
||||||
|
public:
|
||||||
|
virtual ~DiagnosticDisplay() = default;
|
||||||
|
|
||||||
|
// Update the diagnostic indicators for a buffer
|
||||||
|
virtual void updateDiagnostics(const std::string& uri,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) = 0;
|
||||||
|
|
||||||
|
// Show inline diagnostic at cursor position
|
||||||
|
virtual void showInlineDiagnostic(const Diagnostic& diagnostic) = 0;
|
||||||
|
|
||||||
|
// Show diagnostic list/panel
|
||||||
|
virtual void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) = 0;
|
||||||
|
virtual void hideDiagnosticList() = 0;
|
||||||
|
|
||||||
|
// Status bar summary
|
||||||
|
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Terminal Diagnostic Display
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// TerminalDiagnosticDisplay.h
|
||||||
|
class TerminalDiagnosticDisplay : public DiagnosticDisplay {
|
||||||
|
public:
|
||||||
|
explicit TerminalDiagnosticDisplay(TerminalRenderer* renderer);
|
||||||
|
|
||||||
|
void updateDiagnostics(const std::string& uri,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
|
||||||
|
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
void hideDiagnosticList() override;
|
||||||
|
void updateStatusBar(int errorCount, int warningCount) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TerminalRenderer* renderer_;
|
||||||
|
|
||||||
|
// Terminal-specific display strategies
|
||||||
|
void renderGutterMarkers(const std::vector<Diagnostic>& diagnostics);
|
||||||
|
void renderUnderlines(const std::vector<Diagnostic>& diagnostics);
|
||||||
|
void renderVirtualText(const Diagnostic& diagnostic);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terminal Display Strategies:**
|
||||||
|
|
||||||
|
1. **Gutter markers**: Show `E` (error), `W` (warning), `I` (info), `H` (hint) in left gutter
|
||||||
|
2. **Underlines**: Use terminal underline/curly underline capabilities (where supported)
|
||||||
|
3. **Virtual text**: Display diagnostic message at end of line (configurable)
|
||||||
|
4. **Status line**: `[E:3 W:5]` summary
|
||||||
|
5. **Message line**: Full diagnostic on cursor line shown in bottom bar
|
||||||
|
|
||||||
|
```
|
||||||
|
1 │ fn main() {
|
||||||
|
E 2 │ let x: i32 = "hello";
|
||||||
|
3 │ }
|
||||||
|
──────────────────────────────────────
|
||||||
|
error[E0308]: mismatched types
|
||||||
|
expected `i32`, found `&str`
|
||||||
|
[E:1 W:0] main.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 GUI Diagnostic Display
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// GUIDiagnosticDisplay.h
|
||||||
|
class GUIDiagnosticDisplay : public DiagnosticDisplay {
|
||||||
|
public:
|
||||||
|
explicit GUIDiagnosticDisplay(GUIRenderer* renderer, GUITheme* theme);
|
||||||
|
|
||||||
|
void updateDiagnostics(const std::string& uri,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
|
||||||
|
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
void hideDiagnosticList() override;
|
||||||
|
void updateStatusBar(int errorCount, int warningCount) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GUIRenderer* renderer_;
|
||||||
|
GUITheme* theme_;
|
||||||
|
|
||||||
|
// GUI-specific display
|
||||||
|
void renderWavyUnderlines(const std::vector<Diagnostic>& diagnostics);
|
||||||
|
void renderTooltip(Position pos, const Diagnostic& diagnostic);
|
||||||
|
void renderDiagnosticPanel();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**GUI Display Features:**
|
||||||
|
|
||||||
|
1. **Wavy underlines**: Classic IDE-style (red for errors, yellow for warnings, etc.)
|
||||||
|
2. **Gutter icons**: Colored icons/dots in the gutter
|
||||||
|
3. **Hover tooltips**: Rich tooltips on hover showing full diagnostic
|
||||||
|
4. **Diagnostic panel**: Bottom panel with clickable diagnostic list
|
||||||
|
5. **Minimap markers**: Colored marks on the minimap (if present)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. LspManager - Central Coordination
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// LspManager.h
|
||||||
|
class LspManager {
|
||||||
|
public:
|
||||||
|
explicit LspManager(Editor* editor, DiagnosticDisplay* display);
|
||||||
|
|
||||||
|
// Server management
|
||||||
|
void registerServer(const std::string& languageId,
|
||||||
|
const LspServerConfig& config);
|
||||||
|
bool startServerForBuffer(Buffer* buffer);
|
||||||
|
void stopServer(const std::string& languageId);
|
||||||
|
void stopAllServers();
|
||||||
|
|
||||||
|
// Document sync
|
||||||
|
void onBufferOpened(Buffer* buffer);
|
||||||
|
void onBufferChanged(Buffer* buffer);
|
||||||
|
void onBufferClosed(Buffer* buffer);
|
||||||
|
void onBufferSaved(Buffer* buffer);
|
||||||
|
|
||||||
|
// Feature requests
|
||||||
|
void requestCompletion(Buffer* buffer, Position pos,
|
||||||
|
CompletionCallback callback);
|
||||||
|
void requestHover(Buffer* buffer, Position pos,
|
||||||
|
HoverCallback callback);
|
||||||
|
void requestDefinition(Buffer* buffer, Position pos,
|
||||||
|
LocationCallback callback);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
void setDebugLogging(bool enabled);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Editor* editor_;
|
||||||
|
DiagnosticDisplay* display_;
|
||||||
|
DiagnosticStore diagnosticStore_;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<LspClient>> servers_;
|
||||||
|
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
|
||||||
|
|
||||||
|
void handleDiagnostics(const std::string& uri,
|
||||||
|
const std::vector<Diagnostic>& diagnostics);
|
||||||
|
std::string getLanguageId(Buffer* buffer);
|
||||||
|
std::string getUri(Buffer* buffer);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Configuration
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// LspServerConfig.h
|
||||||
|
struct LspServerConfig {
|
||||||
|
std::string command;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
std::vector<std::string> filePatterns; // e.g., {"*.rs", "*.toml"}
|
||||||
|
std::string rootPatterns; // e.g., "Cargo.toml"
|
||||||
|
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
|
||||||
|
bool autostart = true;
|
||||||
|
std::unordered_map<std::string, nlohmann::json> initializationOptions;
|
||||||
|
std::unordered_map<std::string, nlohmann::json> settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default configurations
|
||||||
|
std::vector<LspServerConfig> getDefaultServerConfigs() {
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
.command = "rust-analyzer",
|
||||||
|
.filePatterns = {"*.rs"},
|
||||||
|
.rootPatterns = "Cargo.toml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.command = "clangd",
|
||||||
|
.args = {"--background-index"},
|
||||||
|
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
|
||||||
|
.rootPatterns = "compile_commands.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.command = "gopls",
|
||||||
|
.filePatterns = {"*.go"},
|
||||||
|
.rootPatterns = "go.mod"
|
||||||
|
},
|
||||||
|
// ... more servers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Foundation (2-3 weeks)
|
||||||
|
|
||||||
|
- [ ] JSON-RPC transport layer
|
||||||
|
- [ ] Process management for LSP servers
|
||||||
|
- [ ] Basic `LspClient` with initialize/shutdown
|
||||||
|
- [ ] `textDocument/didOpen`, `textDocument/didClose` (full sync)
|
||||||
|
|
||||||
|
### Phase 2: Incremental Sync (1-2 weeks)
|
||||||
|
|
||||||
|
- [ ] `BufferChangeTracker` integration with `Buffer`
|
||||||
|
- [ ] `textDocument/didChange` with incremental updates
|
||||||
|
- [ ] Change coalescing for rapid edits
|
||||||
|
- [ ] Version tracking
|
||||||
|
|
||||||
|
### Phase 3: Diagnostics (2-3 weeks)
|
||||||
|
|
||||||
|
- [ ] `DiagnosticStore` implementation
|
||||||
|
- [ ] `TerminalDiagnosticDisplay` with gutter markers & status line
|
||||||
|
- [ ] `GUIDiagnosticDisplay` with wavy underlines & tooltips
|
||||||
|
- [ ] `textDocument/publishDiagnostics` handling
|
||||||
|
|
||||||
|
### Phase 4: Language Features (3-4 weeks)
|
||||||
|
|
||||||
|
- [ ] Completion (`textDocument/completion`)
|
||||||
|
- [ ] Hover (`textDocument/hover`)
|
||||||
|
- [ ] Go to definition (`textDocument/definition`)
|
||||||
|
- [ ] Find references (`textDocument/references`)
|
||||||
|
- [ ] Code actions (`textDocument/codeAction`)
|
||||||
|
|
||||||
|
### Phase 5: Polish & Advanced Features (2-3 weeks)
|
||||||
|
|
||||||
|
- [ ] Multiple server support
|
||||||
|
- [ ] Server auto-detection
|
||||||
|
- [ ] Configuration file support
|
||||||
|
- [ ] Workspace symbol search
|
||||||
|
- [ ] Rename refactoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Alignment with kte Core Principles
|
||||||
|
|
||||||
|
### 7.1 Frontend/Backend Separation
|
||||||
|
|
||||||
|
- LSP logic is completely separate from display
|
||||||
|
- `DiagnosticDisplay` interface allows identical behavior across Terminal/GUI
|
||||||
|
- Follows existing pattern: `Renderer`, `InputHandler`, `Frontend`
|
||||||
|
|
||||||
|
### 7.2 Testability
|
||||||
|
|
||||||
|
- `LspClient` is abstract, enabling `MockLspClient` for testing
|
||||||
|
- `DiagnosticDisplay` can be mocked for testing diagnostic flow
|
||||||
|
- Change tracking can be unit tested in isolation
|
||||||
|
|
||||||
|
### 7.3 Performance
|
||||||
|
|
||||||
|
- Incremental sync minimizes data sent to LSP servers
|
||||||
|
- Async message handling doesn't block UI
|
||||||
|
- Diagnostic rendering is batched
|
||||||
|
|
||||||
|
### 7.4 Simplicity
|
||||||
|
|
||||||
|
- Minimal dependencies (nlohmann/json for JSON handling)
|
||||||
|
- Self-contained process management
|
||||||
|
- Clear separation of concerns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
kte/
|
||||||
|
├── lsp/
|
||||||
|
│ ├── LspClient.h
|
||||||
|
│ ├── LspProcessClient.h
|
||||||
|
│ ├── LspProcessClient.cc
|
||||||
|
│ ├── LspManager.h
|
||||||
|
│ ├── LspManager.cc
|
||||||
|
│ ├── LspServerConfig.h
|
||||||
|
│ ├── JsonRpcTransport.h
|
||||||
|
│ ├── JsonRpcTransport.cc
|
||||||
|
│ ├── LspTypes.h # Position, Range, Location, etc.
|
||||||
|
│ ├── Diagnostic.h
|
||||||
|
│ ├── DiagnosticStore.h
|
||||||
|
│ ├── DiagnosticStore.cc
|
||||||
|
│ └── BufferChangeTracker.h
|
||||||
|
├── diagnostic/
|
||||||
|
│ ├── DiagnosticDisplay.h
|
||||||
|
│ ├── TerminalDiagnosticDisplay.h
|
||||||
|
│ ├── TerminalDiagnosticDisplay.cc
|
||||||
|
│ ├── GUIDiagnosticDisplay.h
|
||||||
|
│ └── GUIDiagnosticDisplay.cc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Dependencies
|
||||||
|
|
||||||
|
- **nlohmann/json**: JSON parsing/serialization (header-only)
|
||||||
|
- **POSIX/Windows process APIs**: For spawning LSP servers
|
||||||
|
- Existing kte infrastructure: `Buffer`, `Renderer`, `Frontend`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This plan provides a solid foundation for LSP support while maintaining kte's clean architecture. The key insight is
|
||||||
|
that LSP is fundamentally a backend feature that should be displayed through the existing frontend abstraction layer,
|
||||||
|
ensuring consistent behavior across terminal and GUI modes.
|
||||||
102
docs/syntax on.md
Normal file
102
docs/syntax on.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
### Objective
|
||||||
|
Introduce fast, minimal‑dependency syntax highlighting to kte, consistent with current architecture (Editor/Buffer + GUI/Terminal renderers), preserving ke UX and performance.
|
||||||
|
|
||||||
|
### Guiding principles
|
||||||
|
- Keep core small and fast; no heavy deps (C++17 only).
|
||||||
|
- Start simple (stateless line regex), evolve incrementally (stateful, caching).
|
||||||
|
- Work in both Terminal (ncurses) and GUI (ImGui) with consistent token classes and theme mapping.
|
||||||
|
- Integrate without disrupting existing search highlight, selection, or cursor rendering.
|
||||||
|
|
||||||
|
### Scope of v1
|
||||||
|
- Languages: plain text (off), C/C++ minimal set (keywords, types, strings, chars, comments, numbers, preprocessor).
|
||||||
|
- Stateless per‑line highlighting; handle single‑line comments and strings; defer multi‑line state to v2.
|
||||||
|
- Toggle: `:syntax on|off` and per‑buffer filetype selection.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
1. Core types (new):
|
||||||
|
- `enum class TokenKind { Default, Keyword, Type, String, Char, Comment, Number, Preproc, Constant, Function, Operator, Punctuation, Identifier, Whitespace, Error };`
|
||||||
|
- `struct HighlightSpan { int col_start; int col_end; TokenKind kind; };` // 0‑based columns in buffer indices per rendered line
|
||||||
|
- `struct LineHighlight { std::vector<HighlightSpan> spans; uint64_t version; };`
|
||||||
|
|
||||||
|
2. Interfaces (new):
|
||||||
|
- `class LanguageHighlighter { public: virtual ~LanguageHighlighter() = default; virtual void HighlightLine(const Buffer& buf, int row, std::vector<HighlightSpan>& out) const = 0; virtual bool Stateful() const { return false; } };`
|
||||||
|
- `class HighlighterEngine { public: void SetHighlighter(std::unique_ptr<LanguageHighlighter>); const LineHighlight& GetLine(const Buffer&, int row, uint64_t buf_version); void InvalidateFrom(int row); };`
|
||||||
|
- `class HighlighterRegistry { public: static const LanguageHighlighter& ForFiletype(std::string_view ft); static std::string DetectForPath(std::string_view path, std::string_view first_line); };`
|
||||||
|
|
||||||
|
3. Editor/Buffer integration:
|
||||||
|
- Per‑Buffer settings: `bool syntax_enabled; std::string filetype; std::unique_ptr<HighlighterEngine> highlighter;`
|
||||||
|
- Buffer emits a monotonically increasing `version` on edit; renderers request line highlights by `(row, version)`.
|
||||||
|
- Invalidate cache minimally on edits (v1: current line only; v2: from current line down when stateful constructs present).
|
||||||
|
|
||||||
|
### Rendering integration
|
||||||
|
- TerminalRenderer/GUIRenderer changes:
|
||||||
|
- During line rendering, query `Editor.CurrentBuffer()->highlighter->GetLine(buf, row, buf_version)` to obtain spans.
|
||||||
|
- Apply token styles while drawing glyph runs.
|
||||||
|
- Z‑order and blending:
|
||||||
|
1) Backgrounds (e.g., selection, search highlight rectangles)
|
||||||
|
2) Text with syntax colors
|
||||||
|
3) Cursor/IME decorations
|
||||||
|
- Search highlights must remain visible over syntax colors:
|
||||||
|
- Terminal: combine color/attr with reverse/bold for search; if color conflicts, prefer search.
|
||||||
|
- GUI: draw semi‑transparent rects behind text (already present); keep syntax color for text.
|
||||||
|
|
||||||
|
### Theme and color mapping
|
||||||
|
- Extend `GUITheme.h` with a `SyntaxPalette` mapping `TokenKind -> ImVec4 ink` (and optional background tint for comments/strings disabled by default). Provide default Light/Dark palettes.
|
||||||
|
- Terminal: map `TokenKind` to ncurses color pairs where available; degrade gracefully on 8/16‑color terminals (e.g., comments=dim, keywords=bold, strings=yellow/green if available).
|
||||||
|
|
||||||
|
### Language detection
|
||||||
|
- v1: by file extension; allow manual `:set filetype=<lang>`.
|
||||||
|
- v2: add shebang detection for scripts, simple modelines (optional).
|
||||||
|
|
||||||
|
### Commands/UX
|
||||||
|
- `:syntax on|off` — global default; buffer inherits on open.
|
||||||
|
- `:set filetype=<lang>` — per‑buffer override.
|
||||||
|
- `:syntax reload` — rebuild patterns/themes.
|
||||||
|
- Status line shows filetype and syntax state when changed.
|
||||||
|
|
||||||
|
### Implementation plan (phased)
|
||||||
|
1. Phase 1 — Minimal regex highlighter for C/C++
|
||||||
|
- Implement `CppRegexHighlighter : LanguageHighlighter` with precompiled `std::regex` (or hand‑rolled simple scanners to avoid regex backtracking). Classes: line comment `//…`, block comment start `/*` (no state), string `"…"`, char `'…'` (no multiline), numbers, keywords/types, preprocessor `^\s*#\w+`.
|
||||||
|
- Add `HighlighterEngine` with a simple per‑row cache keyed by `(row, buf_version)`; no background worker.
|
||||||
|
- Integrate into both renderers; add palette to `GUITheme.h`; add terminal color selection.
|
||||||
|
- Add commands.
|
||||||
|
|
||||||
|
2. Phase 2 — Stateful constructs and more languages
|
||||||
|
- Add state machine for multiline comments `/*…*/` and multiline strings (C++11 raw strings), with invalidation from edit line downward until state stabilizes.
|
||||||
|
- Add simple highlighters: JSON (strings, numbers, booleans, null, punctuation), Markdown (headers/emphasis/code fences), Shell (comments, strings, keywords), Go (types, constants, keywords), Python (strings, comments, keywords), Rust (strings, comments, keywords), Lisp (comments, strings, keywords),.
|
||||||
|
- Filetype detection by extension + shebang.
|
||||||
|
|
||||||
|
3. Phase 3 — Performance and caching
|
||||||
|
- Viewport‑first highlighting: compute only visible rows each frame; background task warms cache around viewport.
|
||||||
|
- Reuse span buffers, avoid allocations; small‑vector optimization if needed.
|
||||||
|
- Bench with large files; ensure O(n_visible) cost per frame.
|
||||||
|
|
||||||
|
4. Phase 4 — Extensibility
|
||||||
|
- Public registration API for external highlighters.
|
||||||
|
- Optional Tree‑sitter adapter behind a compile flag (off by default) to keep dependencies minimal.
|
||||||
|
|
||||||
|
### Data flow (per frame)
|
||||||
|
- Renderer asks Editor for Buffer and viewport rows.
|
||||||
|
- For each row: `engine.GetLine(buf, row, buf.version)` → spans.
|
||||||
|
- Renderer emits runs with style from `SyntaxPalette[kind]`.
|
||||||
|
- Search highlights are applied as separate background rectangles (GUI) or attribute toggles (Terminal), not overriding text color.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Unit tests for tokenization per language: golden inputs → spans.
|
||||||
|
- Fuzz/edge cases: escaped quotes, numeric literals, preprocessor lines.
|
||||||
|
- Renderer tests with `TestRenderer` asserting the sequence of style changes for a line.
|
||||||
|
- Performance tests: highlight 1k visible lines repeatedly; assert time under threshold.
|
||||||
|
|
||||||
|
### Risks and mitigations
|
||||||
|
- Regex backtracking/perf: prefer linear scans; precompute keyword tables; avoid nested regex.
|
||||||
|
- Terminal color limitations: feature‑detect colors; provide bold/dim fallbacks.
|
||||||
|
- Stateful correctness: invalidate conservatively (from edit line downward) and cap work per frame.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- New files: `Highlight.h/.cc`, `HighlighterEngine.h/.cc`, `LanguageHighlighter.h`, `CppHighlighter.h/.cc`, optional `HighlighterRegistry.h/.cc`.
|
||||||
|
- Renderer updates: `GUIRenderer.cc`, `TerminalRenderer.cc` to consume spans.
|
||||||
|
- Theming: `GUITheme.h` additions for syntax colors.
|
||||||
|
- Editor/Buffer: per‑buffer syntax settings and highlighter handle.
|
||||||
|
- Commands in `Command.cc` and help text updates.
|
||||||
|
- Docs: README/ROADMAP update and a brief `docs/syntax.md`.
|
||||||
|
- Tests: unit and renderer golden tests.
|
||||||
70
docs/syntax.md
Normal file
70
docs/syntax.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
Syntax highlighting in kte
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
kte provides lightweight syntax highlighting with a pluggable highlighter interface. The initial implementation targets C/C++ and focuses on speed and responsiveness.
|
||||||
|
|
||||||
|
Core types
|
||||||
|
----------
|
||||||
|
|
||||||
|
- `TokenKind` — token categories (keywords, types, strings, comments, numbers, preprocessor, operators, punctuation, identifiers, whitespace, etc.).
|
||||||
|
- `HighlightSpan` — a half-open column range `[col_start, col_end)` with a `TokenKind`.
|
||||||
|
- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version` used to compute it.
|
||||||
|
|
||||||
|
Engine and caching
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- `HighlighterEngine` maintains a per-line cache of `LineHighlight` keyed by row and buffer version.
|
||||||
|
- Cache invalidation occurs when the buffer version changes or when the buffer calls `InvalidateFrom(row)`, which clears cached lines and line states from `row` downward.
|
||||||
|
- The engine supports both stateless and stateful highlighters. For stateful highlighters, it memoizes a simple per-line state and computes lines sequentially when necessary.
|
||||||
|
|
||||||
|
Stateful highlighters
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- `LanguageHighlighter` is the base interface for stateless per-line tokenization.
|
||||||
|
- `StatefulHighlighter` extends it with a `LineState` and the method `HighlightLineStateful(buf, row, prev_state, out)`.
|
||||||
|
- The engine detects `StatefulHighlighter` via dynamic_cast and feeds each line the previous line’s state, caching the resulting state per line.
|
||||||
|
|
||||||
|
C/C++ highlighter
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- `CppHighlighter` implements `StatefulHighlighter`.
|
||||||
|
- Stateless constructs: line comments `//`, strings `"..."`, chars `'...'`, numbers, identifiers (keywords/types), preprocessor at beginning of line after leading whitespace, operators/punctuation, and whitespace.
|
||||||
|
- Stateful constructs (v2):
|
||||||
|
- Multi-line block comments `/* ... */` — the state records whether the next line continues a comment.
|
||||||
|
- Raw strings `R"delim(... )delim"` — the state tracks whether we are inside a raw string and its delimiter `delim` until the closing sequence appears.
|
||||||
|
|
||||||
|
Limitations and TODOs
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- Raw string detection is intentionally simple and does not handle all corner cases of the C++ standard.
|
||||||
|
- Preprocessor handling is line-based; continuation lines with `\\` are not yet tracked.
|
||||||
|
- No semantic analysis; identifiers are classified via small keyword/type sets.
|
||||||
|
- Additional languages (JSON, Markdown, Shell, Python, Go, Rust, Lisp, …) are planned.
|
||||||
|
- Terminal color mapping is conservative to support 8/16-color terminals. Rich color-pair themes can be added later.
|
||||||
|
|
||||||
|
Renderer integration
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
|
||||||
|
- Search highlight and cursor overlays take precedence over syntax colors.
|
||||||
|
|
||||||
|
Extensibility (Phase 4)
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
- Public registration API: external code can register custom highlighters by filetype.
|
||||||
|
- Use `HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
|
||||||
|
- Registered factories are preferred over built-ins for the same filetype key.
|
||||||
|
- Filetype keys are normalized via `HighlighterRegistry::Normalize()`.
|
||||||
|
- Optional Tree-sitter adapter: disabled by default to keep dependencies minimal.
|
||||||
|
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
|
||||||
|
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if needed.
|
||||||
|
- Register a Tree-sitter-backed highlighter for a language (example assumes you link a grammar):
|
||||||
|
```c++
|
||||||
|
extern "C" const TSLanguage* tree_sitter_c();
|
||||||
|
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
|
||||||
|
```
|
||||||
|
- Current adapter is a stub scaffold; it compiles and integrates cleanly when enabled, but
|
||||||
|
intentionally emits no spans until Tree-sitter node-to-token mapping is implemented.
|
||||||
8
kte-cloc
8
kte-cloc
@@ -1,3 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc -fmt 3
|
fmt_arg=""
|
||||||
|
if [ "${V}" = "1" ]
|
||||||
|
then
|
||||||
|
fmt_args="-fmt 3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}
|
||||||
|
|||||||
279
syntax/CppHighlighter.cc
Normal file
279
syntax/CppHighlighter.cc
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#include "CppHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static bool
|
||||||
|
is_digit(char c)
|
||||||
|
{
|
||||||
|
return c >= '0' && c <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CppHighlighter::CppHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
|
||||||
|
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
|
||||||
|
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
|
||||||
|
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
|
||||||
|
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
keywords_.insert(s);
|
||||||
|
const char *types[] = {
|
||||||
|
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
|
||||||
|
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||||
|
"int8_t", "int16_t", "int32_t", "int64_t"
|
||||||
|
};
|
||||||
|
for (auto s: types)
|
||||||
|
types_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
CppHighlighter::is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
CppHighlighter::is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
// Stateless entry simply delegates to stateful with a clean previous state
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
CppHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return state;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
if (s.empty())
|
||||||
|
return state;
|
||||||
|
|
||||||
|
auto push = [&](int a, int b, TokenKind k) {
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
};
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int bol = 0;
|
||||||
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
|
++bol;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
// Continue multi-line raw string from previous line
|
||||||
|
if (state.in_raw_string) {
|
||||||
|
std::string needle = ")" + state.raw_delim + "\"";
|
||||||
|
auto pos = s.find(needle);
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
push(0, n, TokenKind::String);
|
||||||
|
state.in_raw_string = true;
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
int end = static_cast<int>(pos + needle.size());
|
||||||
|
push(0, end, TokenKind::String);
|
||||||
|
i = end;
|
||||||
|
state.in_raw_string = false;
|
||||||
|
state.raw_delim.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue multi-line block comment from previous line
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
// Preprocessor at beginning of line (after leading whitespace)
|
||||||
|
if (i == bol && c == '#') {
|
||||||
|
push(0, n, TokenKind::Preproc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitespace
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line comment
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||||
|
push(i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block comment
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
|
int j = i + 2;
|
||||||
|
bool closed = false;
|
||||||
|
while (j + 1 <= n) {
|
||||||
|
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||||
|
j += 2;
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (closed) {
|
||||||
|
push(i, j, TokenKind::Comment);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Spill to next lines
|
||||||
|
push(i, n, TokenKind::Comment);
|
||||||
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw string start: very simple detection: R"delim(
|
||||||
|
if (c == 'R' && i + 1 < n && s[i + 1] == '"') {
|
||||||
|
int k = i + 2;
|
||||||
|
std::string delim;
|
||||||
|
while (k < n && s[k] != '(') {
|
||||||
|
delim.push_back(s[k]);
|
||||||
|
++k;
|
||||||
|
}
|
||||||
|
if (k < n && s[k] == '(') {
|
||||||
|
int body_start = k + 1;
|
||||||
|
std::string needle = ")" + delim + "\"";
|
||||||
|
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
push(i, n, TokenKind::String);
|
||||||
|
state.in_raw_string = true;
|
||||||
|
state.raw_delim = delim;
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
int end = static_cast<int>(pos + needle.size());
|
||||||
|
push(i, end, TokenKind::String);
|
||||||
|
i = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If malformed, just treat 'R' as identifier fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular string literal
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Char literal
|
||||||
|
if (c == '\'') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\'')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(i, j, TokenKind::Char);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number literal (simple)
|
||||||
|
if (is_digit(c) || (c == '.' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||||
|
s[j] == 'X' || s[j] == 'b' || s[j] == 'B' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier / keyword / type
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
if (keywords_.count(id))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
else if (types_.count(id))
|
||||||
|
k = TokenKind::Type;
|
||||||
|
push(i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operators and punctuation (single char for now)
|
||||||
|
TokenKind kind = TokenKind::Operator;
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
|
||||||
|
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||||
|
']')
|
||||||
|
kind = TokenKind::Punctuation;
|
||||||
|
push(i, i + 1, kind);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
push(i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
35
syntax/CppHighlighter.h
Normal file
35
syntax/CppHighlighter.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// CppHighlighter.h - minimal stateless C/C++ line highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
class Buffer;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class CppHighlighter final : public StatefulHighlighter {
|
||||||
|
public:
|
||||||
|
CppHighlighter();
|
||||||
|
|
||||||
|
~CppHighlighter() override = default;
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> keywords_;
|
||||||
|
std::unordered_set<std::string> types_;
|
||||||
|
|
||||||
|
static bool is_ident_start(char c);
|
||||||
|
|
||||||
|
static bool is_ident_char(char c);
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
159
syntax/ErlangHighlighter.cc
Normal file
159
syntax/ErlangHighlighter.cc
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#include "ErlangHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_' || c == '\'';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '@' || c == ':' || c == '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ErlangHighlighter::ErlangHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"after", "begin", "case", "catch", "cond", "div", "end", "fun", "if", "let", "of",
|
||||||
|
"receive", "when", "try", "rem", "and", "andalso", "orelse", "not", "band", "bor", "bxor",
|
||||||
|
"bnot", "xor", "module", "export", "import", "record", "define", "undef", "include", "include_lib"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErlangHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// comment
|
||||||
|
if (c == '%') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// strings
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// char literal $X
|
||||||
|
if (c == '$') {
|
||||||
|
int j = i + 1;
|
||||||
|
if (j < n && s[j] == '\\' && j + 1 < n)
|
||||||
|
j += 2;
|
||||||
|
else if (j < n)
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Char);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// numbers
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '#' || s[j] == '.' ||
|
||||||
|
s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// atoms/variables/identifiers (including quoted atoms)
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
// quoted atom: '...'
|
||||||
|
if (c == '\'') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (d == '\'') {
|
||||||
|
if (j < n && s[j] == '\'') {
|
||||||
|
++j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (d == '\\')
|
||||||
|
esc = !esc;
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::Identifier);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
// lowercase leading -> atom/function/module; uppercase or '_' -> variable
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
// keyword check (lowercase)
|
||||||
|
std::string lower;
|
||||||
|
lower.reserve(id.size());
|
||||||
|
for (char ch: id)
|
||||||
|
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||||
|
if (kws_.count(lower))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == ',' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c ==
|
||||||
|
'}')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
17
syntax/ErlangHighlighter.h
Normal file
17
syntax/ErlangHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ErlangHighlighter.h - simple Erlang highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class ErlangHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
ErlangHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
121
syntax/ForthHighlighter.cc
Normal file
121
syntax/ForthHighlighter.cc
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "ForthHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_word_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '>' || c == '<' || c == '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ForthHighlighter::ForthHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
":", ";", "if", "else", "then", "begin", "until", "while", "repeat",
|
||||||
|
"do", "loop", "+loop", "leave", "again", "case", "of", "endof", "endcase",
|
||||||
|
".", ".r", ".s", ".\"", ",", "cr", "emit", "type", "key",
|
||||||
|
"+", "-", "*", "/", "mod", "/mod", "+-", "abs", "min", "max",
|
||||||
|
"dup", "drop", "swap", "over", "rot", "-rot", "nip", "tuck", "pick", "roll",
|
||||||
|
"and", "or", "xor", "invert", "lshift", "rshift",
|
||||||
|
"variable", "constant", "value", "to", "create", "does>", "allot", ",",
|
||||||
|
"cells", "cell+", "chars", "char+",
|
||||||
|
"[", "]", "immediate",
|
||||||
|
"s\"", ".\""
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ForthHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// backslash comment to end of line
|
||||||
|
if (c == '\\') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// parenthesis comment ( ... ) if at word boundary
|
||||||
|
if (c == '(') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && s[j] != ')')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Comment);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// strings: ." ... " and S" ... " and raw "..."
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && s[j] != '"')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '#'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// word/identifier
|
||||||
|
if (std::isalpha(static_cast<unsigned char>(c)) || std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_word_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string w = s.substr(i, j - i);
|
||||||
|
// normalize to lowercase for keyword compare (Forth is case-insensitive typically)
|
||||||
|
std::string lower;
|
||||||
|
lower.reserve(w.size());
|
||||||
|
for (char ch: w)
|
||||||
|
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||||
|
TokenKind k = kws_.count(lower) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||||
|
// Single-char punctuation fallback
|
||||||
|
if (w.size() == 1 && std::ispunct(static_cast<unsigned char>(w[0])) && !kws_.count(lower)) {
|
||||||
|
k = (w[0] == '(' || w[0] == ')' || w[0] == ',')
|
||||||
|
? TokenKind::Punctuation
|
||||||
|
: TokenKind::Operator;
|
||||||
|
}
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
17
syntax/ForthHighlighter.h
Normal file
17
syntax/ForthHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ForthHighlighter.h - simple Forth highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class ForthHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
ForthHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
157
syntax/GoHighlighter.cc
Normal file
157
syntax/GoHighlighter.cc
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#include "GoHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
GoHighlighter::GoHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
|
||||||
|
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
|
||||||
|
"switch", "type", "var"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
const char *tp[] = {
|
||||||
|
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
|
||||||
|
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
|
||||||
|
};
|
||||||
|
for (auto s: tp)
|
||||||
|
types_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
int bol = 0;
|
||||||
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
|
++bol;
|
||||||
|
// line comment
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
|
int j = i + 2;
|
||||||
|
bool closed = false;
|
||||||
|
while (j + 1 <= n) {
|
||||||
|
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||||
|
j += 2;
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (!closed) {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
push(out, i, j, TokenKind::Comment);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c == '"' || c == '`') {
|
||||||
|
char q = c;
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
if (q == '`') {
|
||||||
|
while (j < n && s[j] != '`')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
} else {
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||||
|
s[j] == 'X' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
if (kws_.count(id))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
else if (types_.count(id))
|
||||||
|
k = TokenKind::Type;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||||
|
']')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
18
syntax/GoHighlighter.h
Normal file
18
syntax/GoHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// GoHighlighter.h - simple Go highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class GoHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
GoHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
std::unordered_set<std::string> types_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
209
syntax/HighlighterEngine.cc
Normal file
209
syntax/HighlighterEngine.cc
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
#include "HighlighterEngine.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
HighlighterEngine::HighlighterEngine() = default;
|
||||||
|
|
||||||
|
|
||||||
|
HighlighterEngine::~HighlighterEngine()
|
||||||
|
{
|
||||||
|
// stop background worker
|
||||||
|
if (worker_running_.load()) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
worker_running_.store(false);
|
||||||
|
has_request_ = true; // wake it up to exit
|
||||||
|
}
|
||||||
|
cv_.notify_one();
|
||||||
|
if (worker_.joinable())
|
||||||
|
worker_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
hl_ = std::move(hl);
|
||||||
|
cache_.clear();
|
||||||
|
state_cache_.clear();
|
||||||
|
state_last_contig_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const LineHighlight &
|
||||||
|
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mtx_);
|
||||||
|
auto it = cache_.find(row);
|
||||||
|
if (it != cache_.end() && it->second.version == buf_version) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||||
|
LineHighlight &slot = cache_[row];
|
||||||
|
slot.version = buf_version;
|
||||||
|
slot.spans.clear();
|
||||||
|
|
||||||
|
if (!hl_) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||||
|
LanguageHighlighter *hl_ptr = hl_.get();
|
||||||
|
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||||
|
|
||||||
|
if (!is_stateful) {
|
||||||
|
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||||
|
auto &out = slot.spans;
|
||||||
|
lock.unlock();
|
||||||
|
hl_ptr->HighlightLine(buf, row, out);
|
||||||
|
return cache_.at(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||||
|
// but release during heavy computation.
|
||||||
|
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState prev_state;
|
||||||
|
int start_row = -1;
|
||||||
|
if (!state_cache_.empty()) {
|
||||||
|
// linear search over map (unordered), track best candidate
|
||||||
|
int best = -1;
|
||||||
|
for (const auto &kv: state_cache_) {
|
||||||
|
int r = kv.first;
|
||||||
|
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||||
|
if (r > best)
|
||||||
|
best = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best >= 0) {
|
||||||
|
start_row = best;
|
||||||
|
prev_state = state_cache_.at(best).state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||||
|
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||||
|
lock.unlock();
|
||||||
|
StatefulHighlighter::LineState cur_state = prev_state;
|
||||||
|
for (int r = start_row + 1; r <= row; ++r) {
|
||||||
|
std::vector<HighlightSpan> tmp;
|
||||||
|
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||||
|
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||||
|
// Update state cache for r
|
||||||
|
std::lock_guard<std::mutex> gl(mtx_);
|
||||||
|
StateEntry se;
|
||||||
|
se.version = buf_version;
|
||||||
|
se.state = next_state;
|
||||||
|
state_cache_[r] = se;
|
||||||
|
cur_state = next_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return reference under lock to ensure slot's address stability in map
|
||||||
|
lock.lock();
|
||||||
|
return cache_.at(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterEngine::InvalidateFrom(int row)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
if (cache_.empty())
|
||||||
|
return;
|
||||||
|
// Simple implementation: erase all rows >= row
|
||||||
|
for (auto it = cache_.begin(); it != cache_.end();) {
|
||||||
|
if (it->first >= row)
|
||||||
|
it = cache_.erase(it);
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
if (!state_cache_.empty()) {
|
||||||
|
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
|
||||||
|
if (it->first >= row)
|
||||||
|
it = state_cache_.erase(it);
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterEngine::ensure_worker_started() const
|
||||||
|
{
|
||||||
|
if (worker_running_.load())
|
||||||
|
return;
|
||||||
|
worker_running_.store(true);
|
||||||
|
worker_ = std::thread([this]() {
|
||||||
|
this->worker_loop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterEngine::worker_loop() const
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mtx_);
|
||||||
|
while (worker_running_.load()) {
|
||||||
|
cv_.wait(lock, [this]() {
|
||||||
|
return has_request_ || !worker_running_.load();
|
||||||
|
});
|
||||||
|
if (!worker_running_.load())
|
||||||
|
break;
|
||||||
|
WarmRequest req = pending_;
|
||||||
|
has_request_ = false;
|
||||||
|
// Copy locals then release lock while computing
|
||||||
|
lock.unlock();
|
||||||
|
if (req.buf) {
|
||||||
|
int start = std::max(0, req.start_row);
|
||||||
|
int end = std::max(start, req.end_row);
|
||||||
|
for (int r = start; r <= end; ++r) {
|
||||||
|
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||||
|
// Compute line; GetLine is thread-safe
|
||||||
|
(void) this->GetLine(*req.buf, r, req.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||||
|
int warm_margin) const
|
||||||
|
{
|
||||||
|
if (row_count <= 0)
|
||||||
|
return;
|
||||||
|
// Synchronously compute visible rows to ensure cache hits during draw
|
||||||
|
int start = std::max(0, first_row);
|
||||||
|
int end = start + row_count - 1;
|
||||||
|
int max_rows = static_cast<int>(buf.Nrows());
|
||||||
|
if (start >= max_rows)
|
||||||
|
return;
|
||||||
|
if (end >= max_rows)
|
||||||
|
end = max_rows - 1;
|
||||||
|
|
||||||
|
for (int r = start; r <= end; ++r) {
|
||||||
|
(void) GetLine(buf, r, buf_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue background warm-around
|
||||||
|
int warm_start = std::max(0, start - warm_margin);
|
||||||
|
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
|
pending_.buf = &buf;
|
||||||
|
pending_.version = buf_version;
|
||||||
|
pending_.start_row = warm_start;
|
||||||
|
pending_.end_row = warm_end;
|
||||||
|
has_request_ = true;
|
||||||
|
}
|
||||||
|
ensure_worker_started();
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
85
syntax/HighlighterEngine.h
Normal file
85
syntax/HighlighterEngine.h
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// HighlighterEngine.h - caching layer for per-line highlights
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "../Highlight.h"
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
class Buffer;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class HighlighterEngine {
|
||||||
|
public:
|
||||||
|
HighlighterEngine();
|
||||||
|
|
||||||
|
~HighlighterEngine();
|
||||||
|
|
||||||
|
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||||
|
|
||||||
|
// Retrieve highlights for a given line and buffer version.
|
||||||
|
// If cache is stale, recompute using the current highlighter.
|
||||||
|
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||||
|
|
||||||
|
// Invalidate cached lines from row (inclusive)
|
||||||
|
void InvalidateFrom(int row);
|
||||||
|
|
||||||
|
|
||||||
|
bool HasHighlighter() const
|
||||||
|
{
|
||||||
|
return static_cast<bool>(hl_);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Phase 3: viewport-first prefetch and background warming
|
||||||
|
// Compute only the visible range now, and enqueue a background warm-around task.
|
||||||
|
// warm_margin: how many extra lines above/below to warm in the background.
|
||||||
|
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||||
|
int warm_margin = 200) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<LanguageHighlighter> hl_;
|
||||||
|
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||||
|
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||||
|
|
||||||
|
// For stateful highlighters, remember per-line state (state after finishing that row)
|
||||||
|
struct StateEntry {
|
||||||
|
std::uint64_t version{0};
|
||||||
|
// Using the interface type; forward-declare via header
|
||||||
|
StatefulHighlighter::LineState state;
|
||||||
|
};
|
||||||
|
|
||||||
|
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||||
|
|
||||||
|
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||||
|
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||||
|
|
||||||
|
// Thread-safety for caches and background worker state
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
|
||||||
|
// Background warmer
|
||||||
|
struct WarmRequest {
|
||||||
|
const Buffer *buf{nullptr};
|
||||||
|
std::uint64_t version{0};
|
||||||
|
int start_row{0};
|
||||||
|
int end_row{0}; // inclusive
|
||||||
|
};
|
||||||
|
|
||||||
|
mutable std::condition_variable cv_;
|
||||||
|
mutable std::thread worker_;
|
||||||
|
mutable std::atomic<bool> worker_running_{false};
|
||||||
|
mutable bool has_request_{false};
|
||||||
|
mutable WarmRequest pending_{};
|
||||||
|
|
||||||
|
void ensure_worker_started() const;
|
||||||
|
|
||||||
|
void worker_loop() const;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
247
syntax/HighlighterRegistry.cc
Normal file
247
syntax/HighlighterRegistry.cc
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#include "HighlighterRegistry.h"
|
||||||
|
#include "CppHighlighter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
// Forward declare simple highlighters implemented in this project
|
||||||
|
namespace kte {
|
||||||
|
// Registration storage
|
||||||
|
struct RegEntry {
|
||||||
|
std::string ft; // normalized
|
||||||
|
HighlighterRegistry::Factory factory;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static std::vector<RegEntry> &
|
||||||
|
registry()
|
||||||
|
{
|
||||||
|
static std::vector<RegEntry> reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JSONHighlighter;
|
||||||
|
class MarkdownHighlighter;
|
||||||
|
class ShellHighlighter;
|
||||||
|
class GoHighlighter;
|
||||||
|
class PythonHighlighter;
|
||||||
|
class RustHighlighter;
|
||||||
|
class LispHighlighter;
|
||||||
|
class SqlHighlighter;
|
||||||
|
class ErlangHighlighter;
|
||||||
|
class ForthHighlighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers for the above
|
||||||
|
#include "JsonHighlighter.h"
|
||||||
|
#include "MarkdownHighlighter.h"
|
||||||
|
#include "ShellHighlighter.h"
|
||||||
|
#include "GoHighlighter.h"
|
||||||
|
#include "PythonHighlighter.h"
|
||||||
|
#include "RustHighlighter.h"
|
||||||
|
#include "LispHighlighter.h"
|
||||||
|
#include "SqlHighlighter.h"
|
||||||
|
#include "ErlangHighlighter.h"
|
||||||
|
#include "ForthHighlighter.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static std::string
|
||||||
|
to_lower(std::string_view s)
|
||||||
|
{
|
||||||
|
std::string r(s);
|
||||||
|
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
HighlighterRegistry::Normalize(std::string_view ft)
|
||||||
|
{
|
||||||
|
std::string f = to_lower(ft);
|
||||||
|
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
|
||||||
|
return "cpp";
|
||||||
|
if (f == "cpp")
|
||||||
|
return "cpp";
|
||||||
|
if (f == "json")
|
||||||
|
return "json";
|
||||||
|
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
|
||||||
|
return "markdown";
|
||||||
|
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
|
||||||
|
return "shell";
|
||||||
|
if (f == "go" || f == "golang")
|
||||||
|
return "go";
|
||||||
|
if (f == "py" || f == "python")
|
||||||
|
return "python";
|
||||||
|
if (f == "rs" || f == "rust")
|
||||||
|
return "rust";
|
||||||
|
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
|
||||||
|
"cl")
|
||||||
|
return "lisp";
|
||||||
|
if (f == "sql" || f == "sqlite" || f == "sqlite3")
|
||||||
|
return "sql";
|
||||||
|
if (f == "erlang" || f == "erl" || f == "hrl")
|
||||||
|
return "erlang";
|
||||||
|
if (f == "forth" || f == "fth" || f == "4th" || f == "fs")
|
||||||
|
return "forth";
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::unique_ptr<LanguageHighlighter>
|
||||||
|
HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||||
|
{
|
||||||
|
std::string ft = Normalize(filetype);
|
||||||
|
// Prefer externally registered factories
|
||||||
|
for (const auto &e: registry()) {
|
||||||
|
if (e.ft == ft && e.factory)
|
||||||
|
return e.factory();
|
||||||
|
}
|
||||||
|
if (ft == "cpp")
|
||||||
|
return std::make_unique<CppHighlighter>();
|
||||||
|
if (ft == "json")
|
||||||
|
return std::make_unique<JSONHighlighter>();
|
||||||
|
if (ft == "markdown")
|
||||||
|
return std::make_unique<MarkdownHighlighter>();
|
||||||
|
if (ft == "shell")
|
||||||
|
return std::make_unique<ShellHighlighter>();
|
||||||
|
if (ft == "go")
|
||||||
|
return std::make_unique<GoHighlighter>();
|
||||||
|
if (ft == "python")
|
||||||
|
return std::make_unique<PythonHighlighter>();
|
||||||
|
if (ft == "rust")
|
||||||
|
return std::make_unique<RustHighlighter>();
|
||||||
|
if (ft == "lisp")
|
||||||
|
return std::make_unique<LispHighlighter>();
|
||||||
|
if (ft == "sql")
|
||||||
|
return std::make_unique<SqlHighlighter>();
|
||||||
|
if (ft == "erlang")
|
||||||
|
return std::make_unique<ErlangHighlighter>();
|
||||||
|
if (ft == "forth")
|
||||||
|
return std::make_unique<ForthHighlighter>();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
shebang_to_ft(std::string_view first_line)
|
||||||
|
{
|
||||||
|
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
|
||||||
|
return "";
|
||||||
|
std::string low = to_lower(first_line);
|
||||||
|
if (low.find("python") != std::string::npos)
|
||||||
|
return "python";
|
||||||
|
if (low.find("bash") != std::string::npos)
|
||||||
|
return "shell";
|
||||||
|
if (low.find("sh") != std::string::npos)
|
||||||
|
return "shell";
|
||||||
|
if (low.find("zsh") != std::string::npos)
|
||||||
|
return "shell";
|
||||||
|
if (low.find("fish") != std::string::npos)
|
||||||
|
return "shell";
|
||||||
|
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
|
||||||
|
std::string::npos)
|
||||||
|
return "lisp";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||||
|
{
|
||||||
|
// Extension
|
||||||
|
std::string p(path);
|
||||||
|
std::error_code ec;
|
||||||
|
std::string ext = std::filesystem::path(p).extension().string();
|
||||||
|
for (auto &ch: ext)
|
||||||
|
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||||
|
if (!ext.empty()) {
|
||||||
|
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
|
||||||
|
== ".hh")
|
||||||
|
return "cpp";
|
||||||
|
if (ext == ".json")
|
||||||
|
return "json";
|
||||||
|
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
|
||||||
|
return "markdown";
|
||||||
|
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
|
||||||
|
return "shell";
|
||||||
|
if (ext == ".go")
|
||||||
|
return "go";
|
||||||
|
if (ext == ".py")
|
||||||
|
return "python";
|
||||||
|
if (ext == ".rs")
|
||||||
|
return "rust";
|
||||||
|
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|
||||||
|
|| ext == ".cl")
|
||||||
|
return "lisp";
|
||||||
|
if (ext == ".sql" || ext == ".sqlite")
|
||||||
|
return "sql";
|
||||||
|
if (ext == ".erl" || ext == ".hrl")
|
||||||
|
return "erlang";
|
||||||
|
if (ext == ".forth" || ext == ".fth" || ext == ".4th" || ext == ".fs")
|
||||||
|
return "forth";
|
||||||
|
}
|
||||||
|
// Shebang
|
||||||
|
std::string ft = shebang_to_ft(first_line);
|
||||||
|
return ft;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
|
|
||||||
|
// Extensibility API implementations
|
||||||
|
namespace kte {
|
||||||
|
void
|
||||||
|
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||||
|
{
|
||||||
|
std::string ft = Normalize(filetype);
|
||||||
|
for (auto &e: registry()) {
|
||||||
|
if (e.ft == ft) {
|
||||||
|
if (override_existing)
|
||||||
|
e.factory = std::move(factory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||||
|
{
|
||||||
|
std::string ft = Normalize(filetype);
|
||||||
|
for (const auto &e: registry())
|
||||||
|
if (e.ft == ft)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
HighlighterRegistry::RegisteredFiletypes()
|
||||||
|
{
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.reserve(registry().size());
|
||||||
|
for (const auto &e: registry())
|
||||||
|
out.push_back(e.ft);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef KTE_ENABLE_TREESITTER
|
||||||
|
// Forward declare adapter factory
|
||||||
|
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||||
|
const void * (*get_lang)());
|
||||||
|
|
||||||
|
void
|
||||||
|
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||||
|
const TSLanguage * (*get_language)())
|
||||||
|
{
|
||||||
|
std::string ft = Normalize(filetype);
|
||||||
|
Register(ft, [ft, get_language]() {
|
||||||
|
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||||
|
}, /*override_existing=*/true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} // namespace kte
|
||||||
47
syntax/HighlighterRegistry.h
Normal file
47
syntax/HighlighterRegistry.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class HighlighterRegistry {
|
||||||
|
public:
|
||||||
|
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
|
||||||
|
|
||||||
|
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
|
||||||
|
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||||
|
|
||||||
|
// Detect filetype by path extension and shebang (first line).
|
||||||
|
// Returns normalized id or empty string if unknown.
|
||||||
|
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||||
|
|
||||||
|
// Normalize various aliases/extensions to canonical ids.
|
||||||
|
static std::string Normalize(std::string_view ft);
|
||||||
|
|
||||||
|
// Extensibility: allow external code to register highlighters at runtime.
|
||||||
|
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||||
|
// normalized key and override=false, the existing factory is kept.
|
||||||
|
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||||
|
|
||||||
|
// Returns true if a factory is registered for the (normalized) filetype.
|
||||||
|
static bool IsRegistered(std::string_view filetype);
|
||||||
|
|
||||||
|
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||||
|
static std::vector<std::string> RegisteredFiletypes();
|
||||||
|
|
||||||
|
#ifdef KTE_ENABLE_TREESITTER
|
||||||
|
// Forward declaration to avoid hard dependency when disabled.
|
||||||
|
struct TSLanguage;
|
||||||
|
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||||
|
// The getter should return a non-null language pointer for the grammar.
|
||||||
|
static void RegisterTreeSitter(std::string_view filetype,
|
||||||
|
const TSLanguage * (*get_language)());
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
90
syntax/JsonHighlighter.cc
Normal file
90
syntax/JsonHighlighter.cc
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "JsonHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static bool
|
||||||
|
is_digit(char c)
|
||||||
|
{
|
||||||
|
return c >= '0' && c <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
auto push = [&](int a, int b, TokenKind k) {
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
};
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_digit(c) || (c == '-' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'e' ||
|
||||||
|
s[j] == 'E' || s[j] == '+' || s[j] == '-' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// booleans/null
|
||||||
|
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && std::isalpha(static_cast<unsigned char>(s[j])))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
if (id == "true" || id == "false" || id == "null")
|
||||||
|
push(i, j, TokenKind::Constant);
|
||||||
|
else
|
||||||
|
push(i, j, TokenKind::Identifier);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// punctuation
|
||||||
|
if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
|
||||||
|
push(i, i + 1, TokenKind::Punctuation);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// fallback
|
||||||
|
push(i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
12
syntax/JsonHighlighter.h
Normal file
12
syntax/JsonHighlighter.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// JsonHighlighter.h - simple JSON line highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class JSONHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
51
syntax/LanguageHighlighter.h
Normal file
51
syntax/LanguageHighlighter.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// LanguageHighlighter.h - interface for line-based highlighters
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "../Highlight.h"
|
||||||
|
|
||||||
|
class Buffer;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
virtual ~LanguageHighlighter() = default;
|
||||||
|
|
||||||
|
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||||
|
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||||
|
|
||||||
|
|
||||||
|
virtual bool Stateful() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
|
||||||
|
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
|
||||||
|
class StatefulHighlighter : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
struct LineState {
|
||||||
|
bool in_block_comment{false};
|
||||||
|
bool in_raw_string{false};
|
||||||
|
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||||
|
std::string raw_delim;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||||
|
// Implementations should append spans for this line to out and compute the next state.
|
||||||
|
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const = 0;
|
||||||
|
|
||||||
|
|
||||||
|
bool Stateful() const override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
107
syntax/LispHighlighter.cc
Normal file
107
syntax/LispHighlighter.cc
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#include "LispHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LispHighlighter::LispHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
|
||||||
|
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
int bol = 0;
|
||||||
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
|
++bol;
|
||||||
|
if (bol < n && s[bol] == ';') {
|
||||||
|
push(out, bol, n, TokenKind::Comment);
|
||||||
|
if (bol > 0)
|
||||||
|
push(out, 0, bol, TokenKind::Whitespace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == ';') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
|
||||||
|
'_') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
|
||||||
|
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
17
syntax/LispHighlighter.h
Normal file
17
syntax/LispHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// LispHighlighter.h - simple Lisp/Scheme family highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class LispHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
LispHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
132
syntax/MarkdownHighlighter.cc
Normal file
132
syntax/MarkdownHighlighter.cc
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#include "MarkdownHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
LineState st; // not used in stateless entry
|
||||||
|
(void) HighlightLineStateful(buf, row, st, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return state;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
|
||||||
|
// Reuse in_block_comment flag as "in fenced code" state.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
// If line contains closing fence ``` then close after it
|
||||||
|
auto pos = s.find("```");
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
push_span(out, 0, n, TokenKind::String);
|
||||||
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
int end = static_cast<int>(pos + 3);
|
||||||
|
push_span(out, 0, end, TokenKind::String);
|
||||||
|
// rest of line processed normally after fence
|
||||||
|
int i = end;
|
||||||
|
// whitespace
|
||||||
|
if (i < n)
|
||||||
|
push_span(out, i, n, TokenKind::Default);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect fenced code block start at beginning (allow leading spaces)
|
||||||
|
int bol = 0;
|
||||||
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
|
++bol;
|
||||||
|
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
|
||||||
|
push_span(out, bol, n, TokenKind::String);
|
||||||
|
state.in_block_comment = true; // enter fenced mode
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headings: lines starting with 1-6 '#'
|
||||||
|
if (bol < n && s[bol] == '#') {
|
||||||
|
int j = bol;
|
||||||
|
while (j < n && s[j] == '#')
|
||||||
|
++j; // hashes
|
||||||
|
// include following space and text as Keyword to stand out
|
||||||
|
push_span(out, bol, n, TokenKind::Keyword);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process inline: emphasis and code spans
|
||||||
|
int i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == '`') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && s[j] != '`')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
push_span(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '*' || c == '_') {
|
||||||
|
// bold/italic markers: treat the marker and until next same marker as Type to highlight
|
||||||
|
char m = c;
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && s[j] != m)
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
push_span(out, i, j, TokenKind::Type);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// links []() minimal: treat [text](url) as Function
|
||||||
|
if (c == '[') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && s[j] != ']')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j; // include ]
|
||||||
|
if (j < n && s[j] == '(') {
|
||||||
|
while (j < n && s[j] != ')')
|
||||||
|
++j;
|
||||||
|
if (j < n)
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
push_span(out, i, j, TokenKind::Function);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// whitespace
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push_span(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// fallback: default single char
|
||||||
|
push_span(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
14
syntax/MarkdownHighlighter.h
Normal file
14
syntax/MarkdownHighlighter.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// MarkdownHighlighter.h - simple Markdown highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class MarkdownHighlighter final : public StatefulHighlighter {
|
||||||
|
public:
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
17
syntax/NullHighlighter.cc
Normal file
17
syntax/NullHighlighter.cc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#include "NullHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
void
|
||||||
|
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
if (n <= 0)
|
||||||
|
return;
|
||||||
|
out.push_back({0, n, TokenKind::Default});
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
11
syntax/NullHighlighter.h
Normal file
11
syntax/NullHighlighter.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// NullHighlighter.h - default highlighter that emits a single Default span per line
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class NullHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
172
syntax/PythonHighlighter.cc
Normal file
172
syntax/PythonHighlighter.cc
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#include "PythonHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PythonHighlighter::PythonHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
|
||||||
|
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
|
||||||
|
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
LineState st;
|
||||||
|
(void) HighlightLineStateful(buf, row, st, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return state;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
|
||||||
|
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||||
|
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||||
|
auto pos = s.find(state.raw_delim);
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
push(out, 0, n, TokenKind::String);
|
||||||
|
return state; // still inside
|
||||||
|
} else {
|
||||||
|
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||||
|
push(out, 0, end, TokenKind::String);
|
||||||
|
// remainder processed normally
|
||||||
|
s = s.substr(end);
|
||||||
|
n = static_cast<int>(s.size());
|
||||||
|
state.in_raw_string = false;
|
||||||
|
state.raw_delim.clear();
|
||||||
|
// Continue parsing remainder as a separate small loop
|
||||||
|
int base = end;
|
||||||
|
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||||
|
// For simplicity, mark rest as Default
|
||||||
|
if (n > 0)
|
||||||
|
push(out, base, base + n, TokenKind::Default);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
// Detect comment start '#', ignoring inside strings
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '#') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Strings: triple quotes and single-line
|
||||||
|
if (c == '"' || c == '\'') {
|
||||||
|
char q = c;
|
||||||
|
// triple?
|
||||||
|
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
|
||||||
|
std::string delim(3, q);
|
||||||
|
int j = i + 3; // search for closing triple
|
||||||
|
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
push(out, i, n, TokenKind::String);
|
||||||
|
state.in_raw_string = true;
|
||||||
|
state.raw_delim = delim;
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
int end = static_cast<int>(pos + 3);
|
||||||
|
push(out, i, end, TokenKind::String);
|
||||||
|
i = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == q)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
if (kws_.count(id))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
20
syntax/PythonHighlighter.h
Normal file
20
syntax/PythonHighlighter.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// PythonHighlighter.h - simple Python highlighter with triple-quote state
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class PythonHighlighter final : public StatefulHighlighter {
|
||||||
|
public:
|
||||||
|
PythonHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
145
syntax/RustHighlighter.cc
Normal file
145
syntax/RustHighlighter.cc
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#include "RustHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RustHighlighter::RustHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
|
||||||
|
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
|
||||||
|
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
|
||||||
|
"await", "try"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
const char *tp[] = {
|
||||||
|
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
|
||||||
|
"bool", "char", "str"
|
||||||
|
};
|
||||||
|
for (auto s: tp)
|
||||||
|
types_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
|
int j = i + 2;
|
||||||
|
bool closed = false;
|
||||||
|
while (j + 1 <= n) {
|
||||||
|
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||||
|
j += 2;
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (!closed) {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
push(out, i, j, TokenKind::Comment);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c == '"') {
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
if (kws_.count(id))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
else if (types_.count(id))
|
||||||
|
k = TokenKind::Type;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||||
|
']')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
18
syntax/RustHighlighter.h
Normal file
18
syntax/RustHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// RustHighlighter.h - simple Rust highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class RustHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
RustHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
std::unordered_set<std::string> types_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
105
syntax/ShellHighlighter.cc
Normal file
105
syntax/ShellHighlighter.cc
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#include "ShellHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
// if first non-space is '#', whole line is comment
|
||||||
|
int bol = 0;
|
||||||
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
|
++bol;
|
||||||
|
if (bol < n && s[bol] == '#') {
|
||||||
|
push(out, bol, n, TokenKind::Comment);
|
||||||
|
if (bol > 0)
|
||||||
|
push(out, 0, bol, TokenKind::Whitespace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '#') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == '\'' || c == '"') {
|
||||||
|
char q = c;
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (q == '"') {
|
||||||
|
if (esc) {
|
||||||
|
esc = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '"')
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (d == '\'')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// simple keywords
|
||||||
|
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
static const char *kws[] = {
|
||||||
|
"if", "then", "fi", "for", "in", "do", "done", "case", "esac", "while", "function",
|
||||||
|
"elif", "else"
|
||||||
|
};
|
||||||
|
bool kw = false;
|
||||||
|
for (auto k: kws)
|
||||||
|
if (id == k) {
|
||||||
|
kw = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
push(out, i, j, kw ? TokenKind::Keyword : TokenKind::Identifier);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == '(' || c == ')' || c == '{' || c == '}' || c == ',' || c == ';')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
11
syntax/ShellHighlighter.h
Normal file
11
syntax/ShellHighlighter.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// ShellHighlighter.h - simple POSIX shell highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class ShellHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
156
syntax/SqlHighlighter.cc
Normal file
156
syntax/SqlHighlighter.cc
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#include "SqlHighlighter.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
static void
|
||||||
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||||
|
{
|
||||||
|
if (b > a)
|
||||||
|
out.push_back({a, b, k});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_start(char c)
|
||||||
|
{
|
||||||
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_ident_char(char c)
|
||||||
|
{
|
||||||
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SqlHighlighter::SqlHighlighter()
|
||||||
|
{
|
||||||
|
const char *kw[] = {
|
||||||
|
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
|
||||||
|
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
|
||||||
|
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
|
||||||
|
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
|
||||||
|
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
|
||||||
|
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
|
||||||
|
};
|
||||||
|
for (auto s: kw)
|
||||||
|
kws_.insert(s);
|
||||||
|
|
||||||
|
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
|
||||||
|
for (auto s: types)
|
||||||
|
types_.insert(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
const auto &rows = buf.Rows();
|
||||||
|
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||||
|
return;
|
||||||
|
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||||
|
int n = static_cast<int>(s.size());
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == ' ' || c == '\t') {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Whitespace);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// line comments: -- ...
|
||||||
|
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// simple block comment on same line: /* ... */
|
||||||
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
|
int j = i + 2;
|
||||||
|
bool closed = false;
|
||||||
|
while (j + 1 <= n) {
|
||||||
|
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||||
|
j += 2;
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (!closed) {
|
||||||
|
push(out, i, n, TokenKind::Comment);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
push(out, i, j, TokenKind::Comment);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// strings: '...' or "..."
|
||||||
|
if (c == '\'' || c == '"') {
|
||||||
|
char q = c;
|
||||||
|
int j = i + 1;
|
||||||
|
bool esc = false;
|
||||||
|
while (j < n) {
|
||||||
|
char d = s[j++];
|
||||||
|
if (d == q) {
|
||||||
|
// Handle doubled quote escaping for SQL single quotes
|
||||||
|
if (q == '\'' && j < n && s[j] == '\'') {
|
||||||
|
++j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (d == '\\') {
|
||||||
|
esc = !esc;
|
||||||
|
} else {
|
||||||
|
esc = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push(out, i, j, TokenKind::String);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||||
|
++j;
|
||||||
|
push(out, i, j, TokenKind::Number);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_ident_start(c)) {
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < n && is_ident_char(s[j]))
|
||||||
|
++j;
|
||||||
|
std::string id = s.substr(i, j - i);
|
||||||
|
std::string lower;
|
||||||
|
lower.reserve(id.size());
|
||||||
|
for (char ch: id)
|
||||||
|
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||||
|
TokenKind k = TokenKind::Identifier;
|
||||||
|
if (kws_.count(lower))
|
||||||
|
k = TokenKind::Keyword;
|
||||||
|
else if (types_.count(lower))
|
||||||
|
k = TokenKind::Type;
|
||||||
|
push(out, i, j, k);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||||
|
TokenKind k = TokenKind::Operator;
|
||||||
|
if (c == ',' || c == ';' || c == '(' || c == ')')
|
||||||
|
k = TokenKind::Punctuation;
|
||||||
|
push(out, i, i + 1, k);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
18
syntax/SqlHighlighter.h
Normal file
18
syntax/SqlHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// SqlHighlighter.h - simple SQL/SQLite highlighter
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
class SqlHighlighter final : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
SqlHighlighter();
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_set<std::string> kws_;
|
||||||
|
std::unordered_set<std::string> types_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
51
syntax/TreeSitterHighlighter.cc
Normal file
51
syntax/TreeSitterHighlighter.cc
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#include "TreeSitterHighlighter.h"
|
||||||
|
|
||||||
|
#ifdef KTE_ENABLE_TREESITTER
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
|
||||||
|
: language_(lang), filetype_(std::move(filetype)) {}
|
||||||
|
|
||||||
|
|
||||||
|
TreeSitterHighlighter::~TreeSitterHighlighter()
|
||||||
|
{
|
||||||
|
disposeParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
|
||||||
|
{
|
||||||
|
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
|
||||||
|
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||||
|
// and build tree_ from the buffer contents.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TreeSitterHighlighter::disposeParser() const
|
||||||
|
{
|
||||||
|
// Stub; nothing to dispose when not actually creating parser/tree
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
|
||||||
|
{
|
||||||
|
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::unique_ptr<LanguageHighlighter>
|
||||||
|
CreateTreeSitterHighlighter(const char *filetype,
|
||||||
|
const void * (*get_lang)())
|
||||||
|
{
|
||||||
|
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
|
||||||
|
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
|
|
||||||
|
#endif // KTE_ENABLE_TREESITTER
|
||||||
48
syntax/TreeSitterHighlighter.h
Normal file
48
syntax/TreeSitterHighlighter.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// TreeSitterHighlighter.h - optional adapter for Tree-sitter (behind KTE_ENABLE_TREESITTER)
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef KTE_ENABLE_TREESITTER
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "LanguageHighlighter.h"
|
||||||
|
|
||||||
|
// Forward-declare Tree-sitter C API to avoid hard coupling in headers if includes are not present
|
||||||
|
extern "C" {
|
||||||
|
struct TSLanguage;
|
||||||
|
struct TSParser;
|
||||||
|
struct TSTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
|
||||||
|
// does very limited token classification. This acts as a scaffold for future richer
|
||||||
|
// queries. If no queries are provided, it currently produces no spans (safe fallback).
|
||||||
|
class TreeSitterHighlighter : public LanguageHighlighter {
|
||||||
|
public:
|
||||||
|
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
|
||||||
|
|
||||||
|
~TreeSitterHighlighter() override;
|
||||||
|
|
||||||
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TSLanguage *language_{nullptr};
|
||||||
|
std::string filetype_;
|
||||||
|
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||||
|
mutable TSParser *parser_{nullptr};
|
||||||
|
mutable TSTree *tree_{nullptr};
|
||||||
|
|
||||||
|
void ensureParsed(const Buffer &buf) const;
|
||||||
|
|
||||||
|
void disposeParser() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
|
||||||
|
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||||
|
const void * (*get_lang)());
|
||||||
|
} // namespace kte
|
||||||
|
|
||||||
|
#endif // KTE_ENABLE_TREESITTER
|
||||||
177
themes/EInk.h
Normal file
177
themes/EInk.h
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// themes/EInk.h — Monochrome e-ink inspired ImGui themes (header-only)
|
||||||
|
#pragma once
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||||
|
|
||||||
|
static void
|
||||||
|
ApplyEInkImGuiTheme()
|
||||||
|
{
|
||||||
|
// E-Ink grayscale palette (light background)
|
||||||
|
const ImVec4 paper = RGBA(0xF2F2EE); // light paper
|
||||||
|
const ImVec4 bg1 = RGBA(0xE6E6E2);
|
||||||
|
const ImVec4 bg2 = RGBA(0xDADAD5);
|
||||||
|
const ImVec4 bg3 = RGBA(0xCFCFCA);
|
||||||
|
const ImVec4 ink = RGBA(0x111111); // primary text (near black)
|
||||||
|
const ImVec4 dim = RGBA(0x666666); // disabled text
|
||||||
|
const ImVec4 border = RGBA(0xB8B8B3);
|
||||||
|
const ImVec4 accent = RGBA(0x222222); // controls/active
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 0.0f;
|
||||||
|
style.FrameRounding = 0.0f;
|
||||||
|
style.PopupRounding = 0.0f;
|
||||||
|
style.GrabRounding = 0.0f;
|
||||||
|
style.TabRounding = 0.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||||
|
colors[ImGuiCol_WindowBg] = paper;
|
||||||
|
colors[ImGuiCol_ChildBg] = paper;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = border;
|
||||||
|
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
colors[ImGuiCol_FrameBg] = bg2;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||||
|
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBg] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||||
|
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||||
|
colors[ImGuiCol_CheckMark] = accent;
|
||||||
|
colors[ImGuiCol_SliderGrab] = accent;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||||
|
colors[ImGuiCol_Button] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
colors[ImGuiCol_Header] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg2;
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = accent;
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||||
|
colors[ImGuiCol_Tab] = bg2;
|
||||||
|
colors[ImGuiCol_TabHovered] = bg1;
|
||||||
|
colors[ImGuiCol_TabActive] = bg3;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||||
|
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = accent;
|
||||||
|
colors[ImGuiCol_NavHighlight] = accent;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_PlotLines] = accent;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = accent;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ApplyEInkDarkImGuiTheme()
|
||||||
|
{
|
||||||
|
// E-Ink dark variant (dark background, light ink)
|
||||||
|
const ImVec4 paper = RGBA(0x1A1A1A);
|
||||||
|
const ImVec4 bg1 = RGBA(0x222222);
|
||||||
|
const ImVec4 bg2 = RGBA(0x2B2B2B);
|
||||||
|
const ImVec4 bg3 = RGBA(0x343434);
|
||||||
|
const ImVec4 ink = RGBA(0xEDEDEA);
|
||||||
|
const ImVec4 dim = RGBA(0xB5B5B3);
|
||||||
|
const ImVec4 border = RGBA(0x444444);
|
||||||
|
const ImVec4 accent = RGBA(0xDDDDDD);
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 0.0f;
|
||||||
|
style.FrameRounding = 0.0f;
|
||||||
|
style.PopupRounding = 0.0f;
|
||||||
|
style.GrabRounding = 0.0f;
|
||||||
|
style.TabRounding = 0.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||||
|
colors[ImGuiCol_WindowBg] = paper;
|
||||||
|
colors[ImGuiCol_ChildBg] = paper;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = border;
|
||||||
|
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
colors[ImGuiCol_FrameBg] = bg2;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||||
|
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBg] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||||
|
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||||
|
colors[ImGuiCol_CheckMark] = accent;
|
||||||
|
colors[ImGuiCol_SliderGrab] = accent;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||||
|
colors[ImGuiCol_Button] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
colors[ImGuiCol_Header] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg2;
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = accent;
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||||
|
colors[ImGuiCol_Tab] = bg2;
|
||||||
|
colors[ImGuiCol_TabHovered] = bg1;
|
||||||
|
colors[ImGuiCol_TabActive] = bg3;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||||
|
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = accent;
|
||||||
|
colors[ImGuiCol_NavHighlight] = accent;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_PlotLines] = accent;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = accent;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||||
|
}
|
||||||
204
themes/Gruvbox.h
Normal file
204
themes/Gruvbox.h
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// themes/Gruvbox.h — Gruvbox Dark/Light (medium) ImGui themes (header-only)
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||||
|
|
||||||
|
static void
|
||||||
|
ApplyGruvboxDarkMediumTheme()
|
||||||
|
{
|
||||||
|
// Gruvbox (dark, medium) palette
|
||||||
|
const ImVec4 bg0 = RGBA(0x282828); // dark0
|
||||||
|
const ImVec4 bg1 = RGBA(0x3C3836); // dark1
|
||||||
|
const ImVec4 bg2 = RGBA(0x504945); // dark2
|
||||||
|
const ImVec4 bg3 = RGBA(0x665C54); // dark3
|
||||||
|
const ImVec4 fg1 = RGBA(0xEBDBB2); // light1
|
||||||
|
const ImVec4 fg0 = RGBA(0xFBF1C7); // light0
|
||||||
|
// accent colors
|
||||||
|
const ImVec4 yellow = RGBA(0xFABD2F);
|
||||||
|
const ImVec4 blue = RGBA(0x83A598);
|
||||||
|
const ImVec4 aqua = RGBA(0x8EC07C);
|
||||||
|
const ImVec4 orange = RGBA(0xFE8019);
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 4.0f;
|
||||||
|
style.FrameRounding = 3.0f;
|
||||||
|
style.PopupRounding = 4.0f;
|
||||||
|
style.GrabRounding = 3.0f;
|
||||||
|
style.TabRounding = 4.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = fg1;
|
||||||
|
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||||
|
colors[ImGuiCol_WindowBg] = bg0;
|
||||||
|
colors[ImGuiCol_ChildBg] = bg0;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = bg2;
|
||||||
|
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_FrameBg] = bg2;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||||
|
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_TitleBg] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = aqua;
|
||||||
|
colors[ImGuiCol_SliderGrab] = aqua;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg2;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = bg2;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Tab] = bg2;
|
||||||
|
colors[ImGuiCol_TabHovered] = bg1;
|
||||||
|
colors[ImGuiCol_TabActive] = bg3;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||||
|
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = orange;
|
||||||
|
colors[ImGuiCol_NavHighlight] = orange;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_PlotLines] = aqua;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ApplyGruvboxLightMediumTheme()
|
||||||
|
{
|
||||||
|
// Gruvbox (light, medium) palette
|
||||||
|
const ImVec4 bg0 = RGBA(0xFBF1C7); // light0
|
||||||
|
const ImVec4 bg1 = RGBA(0xEBDBB2); // light1
|
||||||
|
const ImVec4 bg2 = RGBA(0xD5C4A1); // light2
|
||||||
|
const ImVec4 bg3 = RGBA(0xBDAE93); // light3
|
||||||
|
const ImVec4 fg1 = RGBA(0x3C3836); // dark1
|
||||||
|
const ImVec4 fg0 = RGBA(0x282828); // dark0
|
||||||
|
// accents
|
||||||
|
const ImVec4 yellow = RGBA(0xB57614);
|
||||||
|
const ImVec4 blue = RGBA(0x076678);
|
||||||
|
const ImVec4 aqua = RGBA(0x427B58);
|
||||||
|
const ImVec4 orange = RGBA(0xAF3A03);
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 4.0f;
|
||||||
|
style.FrameRounding = 3.0f;
|
||||||
|
style.PopupRounding = 4.0f;
|
||||||
|
style.GrabRounding = 3.0f;
|
||||||
|
style.TabRounding = 4.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = fg1;
|
||||||
|
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||||
|
colors[ImGuiCol_WindowBg] = bg0;
|
||||||
|
colors[ImGuiCol_ChildBg] = bg0;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = bg2;
|
||||||
|
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_FrameBg] = bg2;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||||
|
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_TitleBg] = bg1;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = aqua;
|
||||||
|
colors[ImGuiCol_SliderGrab] = aqua;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg2;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = bg2;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Tab] = bg2;
|
||||||
|
colors[ImGuiCol_TabHovered] = bg1;
|
||||||
|
colors[ImGuiCol_TabActive] = bg3;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||||
|
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = orange;
|
||||||
|
colors[ImGuiCol_NavHighlight] = orange;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||||
|
colors[ImGuiCol_PlotLines] = aqua;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||||
|
}
|
||||||
111
themes/Nord.h
Normal file
111
themes/Nord.h
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// themes/Nord.h — Nord-inspired ImGui theme (header-only)
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||||
|
|
||||||
|
static void
|
||||||
|
ApplyNordImGuiTheme()
|
||||||
|
{
|
||||||
|
// Nord palette
|
||||||
|
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
|
||||||
|
const ImVec4 nord1 = RGBA(0x3B4252);
|
||||||
|
const ImVec4 nord2 = RGBA(0x434C5E);
|
||||||
|
const ImVec4 nord3 = RGBA(0x4C566A);
|
||||||
|
const ImVec4 nord4 = RGBA(0xD8DEE9);
|
||||||
|
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
|
||||||
|
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
|
||||||
|
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
|
||||||
|
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
|
||||||
|
const ImVec4 nord12 = RGBA(0xD08770); // orange
|
||||||
|
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
|
||||||
|
// Base style tweaks to suit Nord aesthetics
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 4.0f;
|
||||||
|
style.FrameRounding = 3.0f;
|
||||||
|
style.PopupRounding = 4.0f;
|
||||||
|
style.GrabRounding = 3.0f;
|
||||||
|
style.TabRounding = 4.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Text] = nord4; // primary text
|
||||||
|
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
|
||||||
|
colors[ImGuiCol_WindowBg] = nord0;
|
||||||
|
colors[ImGuiCol_ChildBg] = nord0;
|
||||||
|
colors[ImGuiCol_PopupBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.98f);
|
||||||
|
colors[ImGuiCol_Border] = nord2;
|
||||||
|
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_FrameBg] = nord2;
|
||||||
|
colors[ImGuiCol_FrameBgHovered] = nord3;
|
||||||
|
colors[ImGuiCol_FrameBgActive] = nord1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_TitleBg] = nord1;
|
||||||
|
colors[ImGuiCol_TitleBgActive] = nord2;
|
||||||
|
colors[ImGuiCol_TitleBgCollapsed] = nord1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_MenuBarBg] = nord1;
|
||||||
|
colors[ImGuiCol_ScrollbarBg] = nord10;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = nord3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = nord2;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = nord1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = nord8;
|
||||||
|
colors[ImGuiCol_SliderGrab] = nord8;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = nord9;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = nord3;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = nord2;
|
||||||
|
colors[ImGuiCol_ButtonActive] = nord1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = nord3;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = nord10;
|
||||||
|
colors[ImGuiCol_HeaderActive] = nord10;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = nord2;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = nord10;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = nord9;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = nord9;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Tab] = nord2;
|
||||||
|
colors[ImGuiCol_TabHovered] = nord10;
|
||||||
|
colors[ImGuiCol_TabActive] = nord3;
|
||||||
|
colors[ImGuiCol_TabUnfocused] = nord2;
|
||||||
|
colors[ImGuiCol_TabUnfocusedActive] = nord3;
|
||||||
|
|
||||||
|
// Docking colors omitted for compatibility
|
||||||
|
|
||||||
|
colors[ImGuiCol_TableHeaderBg] = nord2;
|
||||||
|
colors[ImGuiCol_TableBorderStrong] = nord1;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = nord13;
|
||||||
|
colors[ImGuiCol_NavHighlight] = nord9;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||||
|
|
||||||
|
// Plots
|
||||||
|
colors[ImGuiCol_PlotLines] = nord8;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = nord9;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = nord13;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = nord12;
|
||||||
|
}
|
||||||
89
themes/Plan9.h
Normal file
89
themes/Plan9.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// themes/Plan9.h — Plan 9 acme-inspired ImGui theme (header-only)
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||||
|
|
||||||
|
static void
|
||||||
|
ApplyPlan9Theme()
|
||||||
|
{
|
||||||
|
// Acme-like colors
|
||||||
|
const ImVec4 paper = RGBA(0xFFFFE8); // pale yellow paper
|
||||||
|
const ImVec4 pane = RGBA(0xFFF4C1); // slightly deeper for frames
|
||||||
|
const ImVec4 ink = RGBA(0x000000); // black text
|
||||||
|
constexpr auto dim = ImVec4(0, 0, 0, 0.60f);
|
||||||
|
const ImVec4 border = RGBA(0x000000); // 1px black
|
||||||
|
const ImVec4 blue = RGBA(0x0064FF); // acme-ish blue accents
|
||||||
|
const ImVec4 blueH = RGBA(0x4C8DFF); // hover/active
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(6.0f, 6.0f);
|
||||||
|
style.FramePadding = ImVec2(5.0f, 3.0f);
|
||||||
|
style.CellPadding = ImVec2(5.0f, 3.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 0.0f;
|
||||||
|
style.FrameRounding = 0.0f;
|
||||||
|
style.PopupRounding = 0.0f;
|
||||||
|
style.GrabRounding = 0.0f;
|
||||||
|
style.TabRounding = 0.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *c = style.Colors;
|
||||||
|
c[ImGuiCol_Text] = ink;
|
||||||
|
c[ImGuiCol_TextDisabled] = dim;
|
||||||
|
c[ImGuiCol_WindowBg] = paper;
|
||||||
|
c[ImGuiCol_ChildBg] = paper;
|
||||||
|
c[ImGuiCol_PopupBg] = ImVec4(pane.x, pane.y, pane.z, 0.98f);
|
||||||
|
c[ImGuiCol_Border] = border;
|
||||||
|
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
c[ImGuiCol_FrameBg] = pane;
|
||||||
|
c[ImGuiCol_FrameBgHovered] = RGBA(0xFFEBA0);
|
||||||
|
c[ImGuiCol_FrameBgActive] = RGBA(0xFFE387);
|
||||||
|
c[ImGuiCol_TitleBg] = pane;
|
||||||
|
c[ImGuiCol_TitleBgActive] = RGBA(0xFFE8A6);
|
||||||
|
c[ImGuiCol_TitleBgCollapsed] = pane;
|
||||||
|
c[ImGuiCol_MenuBarBg] = pane;
|
||||||
|
c[ImGuiCol_ScrollbarBg] = paper;
|
||||||
|
c[ImGuiCol_ScrollbarGrab] = RGBA(0xEADFA5);
|
||||||
|
c[ImGuiCol_ScrollbarGrabHovered] = RGBA(0xE2D37F);
|
||||||
|
c[ImGuiCol_ScrollbarGrabActive] = RGBA(0xD8C757);
|
||||||
|
c[ImGuiCol_CheckMark] = blue;
|
||||||
|
c[ImGuiCol_SliderGrab] = blue;
|
||||||
|
c[ImGuiCol_SliderGrabActive] = blueH;
|
||||||
|
c[ImGuiCol_Button] = RGBA(0xFFF1B0);
|
||||||
|
c[ImGuiCol_ButtonHovered] = RGBA(0xFFE892);
|
||||||
|
c[ImGuiCol_ButtonActive] = RGBA(0xFFE072);
|
||||||
|
c[ImGuiCol_Header] = RGBA(0xFFF1B0);
|
||||||
|
c[ImGuiCol_HeaderHovered] = RGBA(0xFFE892);
|
||||||
|
c[ImGuiCol_HeaderActive] = RGBA(0xFFE072);
|
||||||
|
c[ImGuiCol_Separator] = border;
|
||||||
|
c[ImGuiCol_SeparatorHovered] = blue;
|
||||||
|
c[ImGuiCol_SeparatorActive] = blueH;
|
||||||
|
c[ImGuiCol_ResizeGrip] = ImVec4(0, 0, 0, 0.12f);
|
||||||
|
c[ImGuiCol_ResizeGripHovered] = ImVec4(blue.x, blue.y, blue.z, 0.67f);
|
||||||
|
c[ImGuiCol_ResizeGripActive] = blueH;
|
||||||
|
c[ImGuiCol_Tab] = RGBA(0xFFE8A6);
|
||||||
|
c[ImGuiCol_TabHovered] = RGBA(0xFFE072);
|
||||||
|
c[ImGuiCol_TabActive] = RGBA(0xFFD859);
|
||||||
|
c[ImGuiCol_TabUnfocused] = RGBA(0xFFE8A6);
|
||||||
|
c[ImGuiCol_TabUnfocusedActive] = RGBA(0xFFD859);
|
||||||
|
c[ImGuiCol_TableHeaderBg] = RGBA(0xFFE8A6);
|
||||||
|
c[ImGuiCol_TableBorderStrong] = border;
|
||||||
|
c[ImGuiCol_TableBorderLight] = ImVec4(0, 0, 0, 0.35f);
|
||||||
|
c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0.04f);
|
||||||
|
c[ImGuiCol_TableRowBgAlt] = ImVec4(0, 0, 0, 0.08f);
|
||||||
|
c[ImGuiCol_TextSelectedBg] = ImVec4(blueH.x, blueH.y, blueH.z, 0.35f);
|
||||||
|
c[ImGuiCol_DragDropTarget] = blue;
|
||||||
|
c[ImGuiCol_NavHighlight] = blue;
|
||||||
|
c[ImGuiCol_NavWindowingHighlight] = ImVec4(0, 0, 0, 0.20f);
|
||||||
|
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0.20f);
|
||||||
|
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0.20f);
|
||||||
|
c[ImGuiCol_PlotLines] = blue;
|
||||||
|
c[ImGuiCol_PlotLinesHovered] = blueH;
|
||||||
|
c[ImGuiCol_PlotHistogram] = blue;
|
||||||
|
c[ImGuiCol_PlotHistogramHovered] = blueH;
|
||||||
|
}
|
||||||
184
themes/Solarized.h
Normal file
184
themes/Solarized.h
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// themes/Solarized.h — Solarized Dark/Light ImGui themes (header-only)
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||||
|
|
||||||
|
static void
|
||||||
|
ApplySolarizedDarkTheme()
|
||||||
|
{
|
||||||
|
// Base colors from Ethan Schoonover Solarized
|
||||||
|
const ImVec4 base03 = RGBA(0x002b36);
|
||||||
|
const ImVec4 base02 = RGBA(0x073642);
|
||||||
|
const ImVec4 base01 = RGBA(0x586e75);
|
||||||
|
const ImVec4 base00 = RGBA(0x657b83);
|
||||||
|
const ImVec4 base0 = RGBA(0x839496);
|
||||||
|
const ImVec4 base1 = RGBA(0x93a1a1);
|
||||||
|
const ImVec4 base2 = RGBA(0xeee8d5);
|
||||||
|
const ImVec4 yellow = RGBA(0xb58900);
|
||||||
|
const ImVec4 orange = RGBA(0xcb4b16);
|
||||||
|
const ImVec4 blue = RGBA(0x268bd2);
|
||||||
|
const ImVec4 cyan = RGBA(0x2aa198);
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 3.0f;
|
||||||
|
style.FrameRounding = 3.0f;
|
||||||
|
style.PopupRounding = 3.0f;
|
||||||
|
style.GrabRounding = 3.0f;
|
||||||
|
style.TabRounding = 3.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *c = style.Colors;
|
||||||
|
c[ImGuiCol_Text] = base0;
|
||||||
|
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
|
||||||
|
c[ImGuiCol_WindowBg] = base03;
|
||||||
|
c[ImGuiCol_ChildBg] = base03;
|
||||||
|
c[ImGuiCol_PopupBg] = ImVec4(base02.x, base02.y, base02.z, 0.98f);
|
||||||
|
c[ImGuiCol_Border] = base02;
|
||||||
|
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
c[ImGuiCol_FrameBg] = base02;
|
||||||
|
c[ImGuiCol_FrameBgHovered] = base01;
|
||||||
|
c[ImGuiCol_FrameBgActive] = base00;
|
||||||
|
c[ImGuiCol_TitleBg] = base02;
|
||||||
|
c[ImGuiCol_TitleBgActive] = base01;
|
||||||
|
c[ImGuiCol_TitleBgCollapsed] = base02;
|
||||||
|
c[ImGuiCol_MenuBarBg] = base02;
|
||||||
|
c[ImGuiCol_ScrollbarBg] = base02;
|
||||||
|
c[ImGuiCol_ScrollbarGrab] = base01;
|
||||||
|
c[ImGuiCol_ScrollbarGrabHovered] = base00;
|
||||||
|
c[ImGuiCol_ScrollbarGrabActive] = blue;
|
||||||
|
c[ImGuiCol_CheckMark] = cyan;
|
||||||
|
c[ImGuiCol_SliderGrab] = cyan;
|
||||||
|
c[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
c[ImGuiCol_Button] = base01;
|
||||||
|
c[ImGuiCol_ButtonHovered] = base00;
|
||||||
|
c[ImGuiCol_ButtonActive] = blue;
|
||||||
|
c[ImGuiCol_Header] = base01;
|
||||||
|
c[ImGuiCol_HeaderHovered] = base00;
|
||||||
|
c[ImGuiCol_HeaderActive] = base00;
|
||||||
|
c[ImGuiCol_Separator] = base01;
|
||||||
|
c[ImGuiCol_SeparatorHovered] = base00;
|
||||||
|
c[ImGuiCol_SeparatorActive] = blue;
|
||||||
|
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
|
||||||
|
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
|
||||||
|
c[ImGuiCol_ResizeGripActive] = blue;
|
||||||
|
c[ImGuiCol_Tab] = base01;
|
||||||
|
c[ImGuiCol_TabHovered] = base00;
|
||||||
|
c[ImGuiCol_TabActive] = base02;
|
||||||
|
c[ImGuiCol_TabUnfocused] = base01;
|
||||||
|
c[ImGuiCol_TabUnfocusedActive] = base02;
|
||||||
|
c[ImGuiCol_TableHeaderBg] = base01;
|
||||||
|
c[ImGuiCol_TableBorderStrong] = base00;
|
||||||
|
c[ImGuiCol_TableBorderLight] = ImVec4(base00.x, base00.y, base00.z, 0.6f);
|
||||||
|
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
|
||||||
|
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
|
||||||
|
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
|
||||||
|
c[ImGuiCol_DragDropTarget] = yellow;
|
||||||
|
c[ImGuiCol_NavHighlight] = blue;
|
||||||
|
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
|
||||||
|
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||||
|
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||||
|
c[ImGuiCol_PlotLines] = cyan;
|
||||||
|
c[ImGuiCol_PlotLinesHovered] = blue;
|
||||||
|
c[ImGuiCol_PlotHistogram] = yellow;
|
||||||
|
c[ImGuiCol_PlotHistogramHovered] = orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ApplySolarizedLightTheme()
|
||||||
|
{
|
||||||
|
// Base colors from Ethan Schoonover Solarized (light variant)
|
||||||
|
const ImVec4 base3 = RGBA(0xfdf6e3);
|
||||||
|
const ImVec4 base2 = RGBA(0xeee8d5);
|
||||||
|
const ImVec4 base1 = RGBA(0x93a1a1);
|
||||||
|
const ImVec4 base0 = RGBA(0x839496);
|
||||||
|
const ImVec4 base00 = RGBA(0x657b83);
|
||||||
|
const ImVec4 base01 = RGBA(0x586e75);
|
||||||
|
const ImVec4 base02 = RGBA(0x073642);
|
||||||
|
const ImVec4 base03 = RGBA(0x002b36);
|
||||||
|
const ImVec4 yellow = RGBA(0xb58900);
|
||||||
|
const ImVec4 orange = RGBA(0xcb4b16);
|
||||||
|
const ImVec4 blue = RGBA(0x268bd2);
|
||||||
|
const ImVec4 cyan = RGBA(0x2aa198);
|
||||||
|
|
||||||
|
ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||||
|
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||||
|
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||||
|
style.ScrollbarSize = 14.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 3.0f;
|
||||||
|
style.FrameRounding = 3.0f;
|
||||||
|
style.PopupRounding = 3.0f;
|
||||||
|
style.GrabRounding = 3.0f;
|
||||||
|
style.TabRounding = 3.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 1.0f;
|
||||||
|
|
||||||
|
ImVec4 *c = style.Colors;
|
||||||
|
c[ImGuiCol_Text] = base00;
|
||||||
|
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
|
||||||
|
c[ImGuiCol_WindowBg] = base3;
|
||||||
|
c[ImGuiCol_ChildBg] = base3;
|
||||||
|
c[ImGuiCol_PopupBg] = ImVec4(base2.x, base2.y, base2.z, 0.98f);
|
||||||
|
c[ImGuiCol_Border] = base1;
|
||||||
|
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||||
|
c[ImGuiCol_FrameBg] = base2;
|
||||||
|
c[ImGuiCol_FrameBgHovered] = base1;
|
||||||
|
c[ImGuiCol_FrameBgActive] = base0;
|
||||||
|
c[ImGuiCol_TitleBg] = base2;
|
||||||
|
c[ImGuiCol_TitleBgActive] = base1;
|
||||||
|
c[ImGuiCol_TitleBgCollapsed] = base2;
|
||||||
|
c[ImGuiCol_MenuBarBg] = base2;
|
||||||
|
c[ImGuiCol_ScrollbarBg] = base2;
|
||||||
|
c[ImGuiCol_ScrollbarGrab] = base1;
|
||||||
|
c[ImGuiCol_ScrollbarGrabHovered] = base0;
|
||||||
|
c[ImGuiCol_ScrollbarGrabActive] = blue;
|
||||||
|
c[ImGuiCol_CheckMark] = cyan;
|
||||||
|
c[ImGuiCol_SliderGrab] = cyan;
|
||||||
|
c[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
c[ImGuiCol_Button] = base1;
|
||||||
|
c[ImGuiCol_ButtonHovered] = base0;
|
||||||
|
c[ImGuiCol_ButtonActive] = blue;
|
||||||
|
c[ImGuiCol_Header] = base1;
|
||||||
|
c[ImGuiCol_HeaderHovered] = base0;
|
||||||
|
c[ImGuiCol_HeaderActive] = base0;
|
||||||
|
c[ImGuiCol_Separator] = base1;
|
||||||
|
c[ImGuiCol_SeparatorHovered] = base0;
|
||||||
|
c[ImGuiCol_SeparatorActive] = blue;
|
||||||
|
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
|
||||||
|
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
|
||||||
|
c[ImGuiCol_ResizeGripActive] = blue;
|
||||||
|
c[ImGuiCol_Tab] = base1;
|
||||||
|
c[ImGuiCol_TabHovered] = base0;
|
||||||
|
c[ImGuiCol_TabActive] = base2;
|
||||||
|
c[ImGuiCol_TabUnfocused] = base1;
|
||||||
|
c[ImGuiCol_TabUnfocusedActive] = base2;
|
||||||
|
c[ImGuiCol_TableHeaderBg] = base1;
|
||||||
|
c[ImGuiCol_TableBorderStrong] = base0;
|
||||||
|
c[ImGuiCol_TableBorderLight] = ImVec4(base0.x, base0.y, base0.z, 0.6f);
|
||||||
|
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
|
||||||
|
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
|
||||||
|
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
|
||||||
|
c[ImGuiCol_DragDropTarget] = yellow;
|
||||||
|
c[ImGuiCol_NavHighlight] = blue;
|
||||||
|
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
|
||||||
|
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||||
|
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
|
||||||
|
c[ImGuiCol_PlotLines] = cyan;
|
||||||
|
c[ImGuiCol_PlotLinesHovered] = blue;
|
||||||
|
c[ImGuiCol_PlotHistogram] = yellow;
|
||||||
|
c[ImGuiCol_PlotHistogramHovered] = orange;
|
||||||
|
}
|
||||||
17
themes/ThemeHelpers.h
Normal file
17
themes/ThemeHelpers.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#ifndef KTE_THEMEHELPERS_H
|
||||||
|
#define KTE_THEMEHELPERS_H
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
|
||||||
|
static ImVec4
|
||||||
|
RGBA(const unsigned int rgb, float a = 1.0f)
|
||||||
|
{
|
||||||
|
const float r = static_cast<float>(rgb >> 16 & 0xFF) / 255.0f;
|
||||||
|
const float g = static_cast<float>(rgb >> 8 & 0xFF) / 255.0f;
|
||||||
|
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
|
||||||
|
return {r, g, b, a};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif //KTE_THEMEHELPERS_H
|
||||||
Reference in New Issue
Block a user